From aaae0627e6ef2635aacee201dca850cf1cb7686c Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Wed, 4 Sep 2024 12:10:17 -0400
Subject: [PATCH 01/87] feat(amazonqFeatureDev): enable stop generating button
---
.../webview/ui/apps/featureDevChatConnector.ts | 1 +
.../core/src/amazonq/webview/ui/connector.ts | 17 ++++++-----------
.../src/amazonq/webview/ui/followUps/handler.ts | 1 +
packages/core/src/amazonq/webview/ui/main.ts | 9 +++++++++
.../amazonq/webview/ui/messages/controller.ts | 2 ++
.../src/amazonq/webview/ui/messages/handler.ts | 1 +
.../amazonq/webview/ui/quickActions/handler.ts | 3 +++
7 files changed, 23 insertions(+), 11 deletions(-)
diff --git a/packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts
index 37657c803cf..661849637cd 100644
--- a/packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts
+++ b/packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts
@@ -267,6 +267,7 @@ export class Connector {
this.sendMessageToExtension({
tabID: tabID,
command: 'stop-response',
+ tabType: 'featuredev',
})
}
diff --git a/packages/core/src/amazonq/webview/ui/connector.ts b/packages/core/src/amazonq/webview/ui/connector.ts
index 12cdad34893..70e5b334e25 100644
--- a/packages/core/src/amazonq/webview/ui/connector.ts
+++ b/packages/core/src/amazonq/webview/ui/connector.ts
@@ -156,6 +156,12 @@ export class Connector {
this.gumbyChatConnector.transform(tabID)
}
+ onStopChatResponse = (tabID: string): void => {
+ if (this.tabsStorage.getTab(tabID)?.type === 'featuredev') {
+ this.featureDevChatConnector.onStopChatResponse(tabID)
+ }
+ }
+
handleMessageReceive = async (message: MessageEvent): Promise => {
if (message.data === undefined) {
return
@@ -368,17 +374,6 @@ export class Connector {
}
}
- onStopChatResponse = (tabID: string): void => {
- switch (this.tabsStorage.getTab(tabID)?.type) {
- case 'featuredev':
- this.featureDevChatConnector.onStopChatResponse(tabID)
- break
- case 'cwc':
- this.cwChatConnector.onStopChatResponse(tabID)
- break
- }
- }
-
sendFeedback = (tabId: string, feedbackPayload: FeedbackPayload): void | undefined => {
switch (this.tabsStorage.getTab(tabId)?.type) {
case 'featuredev':
diff --git a/packages/core/src/amazonq/webview/ui/followUps/handler.ts b/packages/core/src/amazonq/webview/ui/followUps/handler.ts
index 162be107c7c..7e3aa65c8e5 100644
--- a/packages/core/src/amazonq/webview/ui/followUps/handler.ts
+++ b/packages/core/src/amazonq/webview/ui/followUps/handler.ts
@@ -46,6 +46,7 @@ export class FollowUpInteractionHandler {
if (followUp.prompt !== undefined) {
this.mynahUI.updateStore(tabID, {
loadingChat: true,
+ cancelButtonWhenLoading: false,
promptInputDisabledState: true,
})
this.mynahUI.addChatItem(tabID, {
diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts
index 300988ac54b..b3d80962d58 100644
--- a/packages/core/src/amazonq/webview/ui/main.ts
+++ b/packages/core/src/amazonq/webview/ui/main.ts
@@ -146,6 +146,7 @@ export const createMynahUI = (ideApi: any, amazonQEnabled: boolean) => {
mynahUI.updateStore(tabID, {
loadingChat: true,
promptInputDisabledState: true,
+ cancelButtonWhenLoading: true,
})
if (message && messageId) {
@@ -221,6 +222,7 @@ export const createMynahUI = (ideApi: any, amazonQEnabled: boolean) => {
) {
mynahUI.updateStore(tabID, {
loadingChat: true,
+ cancelButtonWhenLoading: false,
promptInputDisabledState: true,
})
@@ -377,6 +379,13 @@ export const createMynahUI = (ideApi: any, amazonQEnabled: boolean) => {
onTabRemove: connector.onTabRemove,
onTabChange: connector.onTabChange,
// TODO: update mynah-ui this type doesn't seem correct https://github.com/aws/mynah-ui/blob/3777a39eb534a91fd6b99d6cf421ce78ee5c7526/src/main.ts#L372
+ onStopChatResponse: (tabID: string) => {
+ mynahUI.updateStore(tabID, {
+ loadingChat: false,
+ promptInputDisabledState: false,
+ })
+ connector.onStopChatResponse(tabID)
+ },
onChatPrompt: (tabID: string, prompt: ChatPrompt, eventId: string | undefined) => {
if ((prompt.prompt ?? '') === '' && (prompt.command ?? '') === '') {
return
diff --git a/packages/core/src/amazonq/webview/ui/messages/controller.ts b/packages/core/src/amazonq/webview/ui/messages/controller.ts
index a63034c637a..ca33071f2f1 100644
--- a/packages/core/src/amazonq/webview/ui/messages/controller.ts
+++ b/packages/core/src/amazonq/webview/ui/messages/controller.ts
@@ -74,6 +74,7 @@ export class MessageController {
this.mynahUI.updateStore(selectedTab.id, {
loadingChat: true,
+ cancelButtonWhenLoading: false,
promptInputDisabledState: true,
})
this.mynahUI.addChatItem(selectedTab.id, message)
@@ -104,6 +105,7 @@ export class MessageController {
this.mynahUI.updateStore(newTabID, {
loadingChat: true,
+ cancelButtonWhenLoading: false,
promptInputDisabledState: true,
})
diff --git a/packages/core/src/amazonq/webview/ui/messages/handler.ts b/packages/core/src/amazonq/webview/ui/messages/handler.ts
index 590b9c11aea..2ba3460f749 100644
--- a/packages/core/src/amazonq/webview/ui/messages/handler.ts
+++ b/packages/core/src/amazonq/webview/ui/messages/handler.ts
@@ -35,6 +35,7 @@ export class TextMessageHandler {
this.mynahUI.updateStore(tabID, {
loadingChat: true,
+ cancelButtonWhenLoading: false,
promptInputDisabledState: true,
})
diff --git a/packages/core/src/amazonq/webview/ui/quickActions/handler.ts b/packages/core/src/amazonq/webview/ui/quickActions/handler.ts
index f149f331c5c..c2c60cf94e2 100644
--- a/packages/core/src/amazonq/webview/ui/quickActions/handler.ts
+++ b/packages/core/src/amazonq/webview/ui/quickActions/handler.ts
@@ -79,6 +79,7 @@ export class QuickActionHandler {
if (this.tabsStorage.getTab(affectedTabId)?.type !== 'unknown') {
affectedTabId = this.mynahUI.updateStore('', {
loadingChat: true,
+ cancelButtonWhenLoading: false,
})
}
@@ -103,6 +104,7 @@ export class QuickActionHandler {
// disable chat prompt
this.mynahUI.updateStore(affectedTabId, {
loadingChat: true,
+ cancelButtonWhenLoading: false,
})
this.connector.transform(affectedTabId)
@@ -161,6 +163,7 @@ export class QuickActionHandler {
this.mynahUI.updateStore(affectedTabId, {
loadingChat: true,
promptInputDisabledState: true,
+ cancelButtonWhenLoading: false,
})
void this.connector.requestGenerativeAIAnswer(affectedTabId, {
From c385dfca1c023ba4b887ffca347ff9db90b936a0 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Fri, 6 Sep 2024 16:20:56 -0400
Subject: [PATCH 02/87] feat(amazonqFeatureDev): include stop generation based
on token cancellation
---
packages/core/package.nls.json | 1 +
.../controllers/chat/controller.ts | 66 +++++++++++--------
.../src/amazonqFeatureDev/session/session.ts | 1 +
.../amazonqFeatureDev/session/sessionState.ts | 36 ++++++----
packages/core/src/amazonqFeatureDev/types.ts | 1 +
5 files changed, 64 insertions(+), 41 deletions(-)
diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json
index da644ab59ef..467c7457dec 100644
--- a/packages/core/package.nls.json
+++ b/packages/core/package.nls.json
@@ -296,6 +296,7 @@
"AWS.amazonq.featureDev.pillText.generatingCode": "Generating code...",
"AWS.amazonq.featureDev.pillText.requestingChanges": "Requesting changes ...",
"AWS.amazonq.featureDev.pillText.insertCode": "Accept code",
+ "AWS.amazonq.featureDev.pillText.stoppedCodeGeneration": "You've stopped the code generation.",
"AWS.amazonq.featureDev.pillText.sendFeedback": "Send feedback",
"AWS.amazonq.featureDev.pillText.selectFiles": "Select files for context",
"AWS.amazonq.featureDev.pillText.retry": "Retry",
diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
index cbd6a2296f0..3ebf87bbebb 100644
--- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
+++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
@@ -349,6 +349,13 @@ export class FeatureDevController {
await session.send(message)
const filePaths = session.state.filePaths ?? []
const deletedFiles = session.state.deletedFiles ?? []
+
+ // Only add the follow up accept/deny buttons when the tab hasn't been closed/request hasn't been cancelled
+ if (session?.state.tokenSource.token.isCancellationRequested) {
+ this.workOnNewTask(message)
+ return
+ }
+
if (filePaths.length === 0 && deletedFiles.length === 0) {
this.messenger.sendAnswer({
message: i18n('AWS.amazonq.featureDev.pillText.unableGenerateChanges'),
@@ -375,11 +382,6 @@ export class FeatureDevController {
return
}
- // Only add the follow up accept/deny buttons when the tab hasn't been closed/request hasn't been cancelled
- if (session?.state.tokenSource.token.isCancellationRequested) {
- return
- }
-
this.messenger.sendCodeResult(
filePaths,
deletedFiles,
@@ -429,7 +431,31 @@ export class FeatureDevController {
}
}
}
+ private workOnNewTask(message: any) {
+ this.messenger.sendAnswer({
+ type: 'system-prompt',
+ tabID: message.tabID,
+ followUps: [
+ {
+ pillText: i18n('AWS.amazonq.featureDev.pillText.newTask'),
+ type: FollowUpTypes.NewTask,
+ status: 'info',
+ },
+ {
+ pillText: i18n('AWS.amazonq.featureDev.pillText.closeSession'),
+ type: FollowUpTypes.CloseSession,
+ status: 'info',
+ },
+ ],
+ })
+ // Ensure that chat input is enabled so that they can provide additional iterations if they choose
+ this.messenger.sendChatInputEnabled(message.tabID, true)
+ this.messenger.sendUpdatePlaceholder(
+ message.tabID,
+ i18n('AWS.amazonq.featureDev.placeholder.additionalImprovements')
+ )
+ }
// TODO add type
private async insertCode(message: any) {
let session
@@ -449,7 +475,6 @@ export class FeatureDevController {
result: 'Succeeded',
})
await session.insertChanges()
-
this.messenger.sendAnswer({
type: 'answer',
tabID: message.tabID,
@@ -457,27 +482,7 @@ export class FeatureDevController {
canBeVoted: true,
})
- this.messenger.sendAnswer({
- type: 'system-prompt',
- tabID: message.tabID,
- followUps: [
- {
- pillText: i18n('AWS.amazonq.featureDev.pillText.newTask'),
- type: FollowUpTypes.NewTask,
- status: 'info',
- },
- {
- pillText: i18n('AWS.amazonq.featureDev.pillText.closeSession'),
- type: FollowUpTypes.CloseSession,
- status: 'info',
- },
- ],
- })
-
- this.messenger.sendUpdatePlaceholder(
- message.tabID,
- i18n('AWS.amazonq.featureDev.placeholder.additionalImprovements')
- )
+ this.workOnNewTask(message)
} catch (err: any) {
this.messenger.sendErrorMessage(
createUserFacingErrorMessage(`Failed to insert code changes: ${err.message}`),
@@ -693,6 +698,13 @@ export class FeatureDevController {
}
private async stopResponse(message: any) {
+ this.messenger.sendAnswer({
+ message: i18n('AWS.amazonq.featureDev.pillText.stoppedCodeGeneration'),
+ type: 'answer-part',
+ tabID: message.tabID,
+ })
+ this.workOnNewTask(message)
+
const session = await this.sessionStorage.getSession(message.tabID)
session.state.tokenSource.cancel()
}
diff --git a/packages/core/src/amazonqFeatureDev/session/session.ts b/packages/core/src/amazonqFeatureDev/session/session.ts
index 01e6c9a2e45..aec75383a11 100644
--- a/packages/core/src/amazonqFeatureDev/session/session.ts
+++ b/packages/core/src/amazonqFeatureDev/session/session.ts
@@ -130,6 +130,7 @@ export class Session {
fs: this.config.fs,
messenger: this.messenger,
telemetry: this.telemetry,
+ tokenSource: this.state.tokenSource,
})
if (resp.nextState) {
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index cd673fd5f6c..3f80699704e 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -121,7 +121,7 @@ function getDeletedFileInfos(deletedFiles: string[], workspaceFolders: CurrentWs
abstract class CodeGenBase {
private pollCount = 180
private requestDelay = 10000
- readonly tokenSource: vscode.CancellationTokenSource
+ public tokenSource: vscode.CancellationTokenSource
public phase: SessionStatePhase = DevPhase.CODEGEN
public readonly conversationId: string
public readonly uploadId: string
@@ -267,6 +267,10 @@ export class CodeGenState extends CodeGenBase implements SessionState {
credentialStartUrl: AuthUtil.instance.startUrl,
})
+ action.tokenSource?.token.onCancellationRequested(() => {
+ if (action.tokenSource) this.tokenSource = action.tokenSource
+ })
+
action.telemetry.setGenerateCodeIteration(this.currentIteration)
action.telemetry.setGenerateCodeLastInvocationTime()
@@ -276,15 +280,17 @@ export class CodeGenState extends CodeGenBase implements SessionState {
action.msg
)
- action.messenger.sendAnswer({
- message: i18n('AWS.amazonq.featureDev.pillText.generatingCode'),
- type: 'answer-part',
- tabID: this.tabID,
- })
- action.messenger.sendUpdatePlaceholder(
- this.tabID,
- i18n('AWS.amazonq.featureDev.pillText.generatingCode')
- )
+ if (!this.tokenSource.token.isCancellationRequested) {
+ action.messenger.sendAnswer({
+ message: i18n('AWS.amazonq.featureDev.pillText.generatingCode'),
+ type: 'answer-part',
+ tabID: this.tabID,
+ })
+ action.messenger.sendUpdatePlaceholder(
+ this.tabID,
+ i18n('AWS.amazonq.featureDev.pillText.generatingCode')
+ )
+ }
const codeGeneration = await this.generateCode({
messenger: action.messenger,
@@ -310,7 +316,8 @@ export class CodeGenState extends CodeGenBase implements SessionState {
this.tabID,
this.currentIteration + 1,
this.codeGenerationRemainingIterationCount,
- this.codeGenerationTotalIterationCount
+ this.codeGenerationTotalIterationCount,
+ this.tokenSource
)
return {
nextState,
@@ -411,10 +418,10 @@ export class MockCodeGenState implements SessionState {
}
export class PrepareCodeGenState implements SessionState {
- public tokenSource: vscode.CancellationTokenSource
public readonly phase = DevPhase.CODEGEN
public uploadId: string
public conversationId: string
+ public tokenSource: vscode.CancellationTokenSource
constructor(
private config: SessionStateConfig,
public filePaths: NewFileInfo[],
@@ -423,9 +430,10 @@ export class PrepareCodeGenState implements SessionState {
public tabID: string,
private currentIteration: number,
public codeGenerationRemainingIterationCount?: number,
- public codeGenerationTotalIterationCount?: number
+ public codeGenerationTotalIterationCount?: number,
+ public superTokenSource?: vscode.CancellationTokenSource
) {
- this.tokenSource = new vscode.CancellationTokenSource()
+ this.tokenSource = superTokenSource || new vscode.CancellationTokenSource()
this.uploadId = config.uploadId
this.conversationId = config.conversationId
}
diff --git a/packages/core/src/amazonqFeatureDev/types.ts b/packages/core/src/amazonqFeatureDev/types.ts
index fafe26a9e24..21eb67ac7f0 100644
--- a/packages/core/src/amazonqFeatureDev/types.ts
+++ b/packages/core/src/amazonqFeatureDev/types.ts
@@ -82,6 +82,7 @@ export interface SessionStateAction {
messenger: Messenger
fs: VirtualFileSystem
telemetry: TelemetryHelper
+ tokenSource?: CancellationTokenSource
}
export type NewFileZipContents = { zipFilePath: string; fileContent: string }
From e05a87a3b89a216536de2522e18dc511d2bceccf Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Tue, 10 Sep 2024 12:14:17 -0400
Subject: [PATCH 03/87] fix(amazonqFeatureDev): move work on new task after
session is closed
---
.../core/src/amazonqFeatureDev/controllers/chat/controller.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
index 3ebf87bbebb..fd3cb7ec736 100644
--- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
+++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
@@ -703,10 +703,11 @@ export class FeatureDevController {
type: 'answer-part',
tabID: message.tabID,
})
- this.workOnNewTask(message)
const session = await this.sessionStorage.getSession(message.tabID)
session.state.tokenSource.cancel()
+
+ this.workOnNewTask(message)
}
private async tabOpened(message: any) {
From b0130420db6ac5e15ad0c205d2dfdb802cdf9521 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Wed, 11 Sep 2024 14:49:11 -0400
Subject: [PATCH 04/87] refactor(amazonqFeatureDev): include updated rts model
---
.../codewhispererruntime-2022-11-11.json | 128 ++++++++++++++++--
.../amazonqFeatureDev/client/featureDev.ts | 12 +-
.../controllers/chat/controller.ts | 13 +-
.../src/amazonqFeatureDev/session/session.ts | 7 +-
.../amazonqFeatureDev/session/sessionState.ts | 83 +++++++-----
packages/core/src/amazonqFeatureDev/types.ts | 5 +-
6 files changed, 189 insertions(+), 59 deletions(-)
diff --git a/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json b/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json
index c8690b720ee..b487286e0ca 100644
--- a/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json
+++ b/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json
@@ -353,8 +353,8 @@
},
"AppStudioStatePropertyValueString": {
"type": "string",
- "max": 1024,
- "min": 1,
+ "max": 10240,
+ "min": 0,
"sensitive": true
},
"ArtifactId": {
@@ -613,6 +613,17 @@
},
"exception": true
},
+ "ConsoleState": {
+ "type": "structure",
+ "members": {
+ "region": { "shape": "String" },
+ "consoleUrl": { "shape": "SensitiveString" },
+ "serviceId": { "shape": "String" },
+ "serviceConsolePage": { "shape": "String" },
+ "serviceSubconsolePage": { "shape": "String" },
+ "taskName": { "shape": "SensitiveString" }
+ }
+ },
"ContentChecksumType": {
"type": "string",
"enum": ["SHA_256"]
@@ -811,7 +822,8 @@
"members": {
"document": { "shape": "TextDocument" },
"cursorState": { "shape": "CursorState" },
- "relevantDocuments": { "shape": "RelevantDocumentList" }
+ "relevantDocuments": { "shape": "RelevantDocumentList" },
+ "useRelevantDocuments": { "shape": "Boolean" }
}
},
"EnvState": {
@@ -1093,6 +1105,21 @@
"max": 10,
"min": 0
},
+ "InlineChatEvent": {
+ "type": "structure",
+ "members": {
+ "inputLength": { "shape": "PrimitiveInteger" },
+ "numSelectedLines": { "shape": "PrimitiveInteger" },
+ "codeIntent": { "shape": "Boolean" },
+ "userDecision": { "shape": "InlineChatUserDecision" },
+ "responseStartLatency": { "shape": "Double" },
+ "responseEndLatency": { "shape": "Double" }
+ }
+ },
+ "InlineChatUserDecision": {
+ "type": "string",
+ "enum": ["ACCEPT", "REJECT", "DISMISS"]
+ },
"Integer": {
"type": "integer",
"box": true
@@ -1535,7 +1562,9 @@
"required": ["conversationState", "workspaceState"],
"members": {
"conversationState": { "shape": "ConversationState" },
- "workspaceState": { "shape": "WorkspaceState" }
+ "workspaceState": { "shape": "WorkspaceState" },
+ "taskAssistPlan": { "shape": "TaskAssistPlan" },
+ "currentCodeGenerationId": { "shape": "CodeGenerationId" }
}
},
"StartTaskAssistCodeGenerationResponse": {
@@ -1648,6 +1677,46 @@
"type": "string",
"enum": ["DECLARATION", "USAGE"]
},
+ "TaskAssistPlan": {
+ "type": "list",
+ "member": { "shape": "TaskAssistPlanStep" },
+ "min": 0
+ },
+ "TaskAssistPlanStep": {
+ "type": "structure",
+ "required": ["filePath", "description"],
+ "members": {
+ "filePath": { "shape": "TaskAssistPlanStepFilePathString" },
+ "description": { "shape": "TaskAssistPlanStepDescriptionString" },
+ "startLine": { "shape": "TaskAssistPlanStepStartLineInteger" },
+ "endLine": { "shape": "TaskAssistPlanStepEndLineInteger" },
+ "action": { "shape": "TaskAssistPlanStepAction" }
+ }
+ },
+ "TaskAssistPlanStepAction": {
+ "type": "string",
+ "enum": ["MODIFY", "CREATE", "DELETE", "UNKNOWN"]
+ },
+ "TaskAssistPlanStepDescriptionString": {
+ "type": "string",
+ "max": 1024,
+ "min": 1
+ },
+ "TaskAssistPlanStepEndLineInteger": {
+ "type": "integer",
+ "box": true,
+ "min": 0
+ },
+ "TaskAssistPlanStepFilePathString": {
+ "type": "string",
+ "max": 1024,
+ "min": 1
+ },
+ "TaskAssistPlanStepStartLineInteger": {
+ "type": "integer",
+ "box": true,
+ "min": 0
+ },
"TaskAssistPlanningUploadContext": {
"type": "structure",
"required": ["conversationId"],
@@ -1668,7 +1737,8 @@
"chatInteractWithMessageEvent": { "shape": "ChatInteractWithMessageEvent" },
"chatUserModificationEvent": { "shape": "ChatUserModificationEvent" },
"terminalUserInteractionEvent": { "shape": "TerminalUserInteractionEvent" },
- "featureDevEvent": { "shape": "FeatureDevEvent" }
+ "featureDevEvent": { "shape": "FeatureDevEvent" },
+ "inlineChatEvent": { "shape": "InlineChatEvent" }
},
"union": true
},
@@ -1777,7 +1847,7 @@
},
"TransformationDownloadArtifactType": {
"type": "string",
- "enum": ["ClientInstructions", "Logs"]
+ "enum": ["ClientInstructions", "Logs", "GeneratedCode"]
},
"TransformationDownloadArtifacts": {
"type": "list",
@@ -1808,7 +1878,15 @@
},
"TransformationLanguage": {
"type": "string",
- "enum": ["JAVA_8", "JAVA_11", "JAVA_17", "C_SHARP"]
+ "enum": ["JAVA_8", "JAVA_11", "JAVA_17", "C_SHARP", "COBOL", "PL_I", "JCL"]
+ },
+ "TransformationLanguages": {
+ "type": "list",
+ "member": { "shape": "TransformationLanguage" }
+ },
+ "TransformationMainframeRuntimeEnv": {
+ "type": "string",
+ "enum": ["MAINFRAME"]
},
"TransformationOperatingSystemFamily": {
"type": "string",
@@ -1841,24 +1919,40 @@
},
"TransformationProgressUpdateStatus": {
"type": "string",
- "enum": ["IN_PROGRESS", "COMPLETED", "FAILED", "PAUSED"]
+ "enum": ["IN_PROGRESS", "COMPLETED", "FAILED", "PAUSED", "AWAITING_CLIENT_ACTION"]
+ },
+ "TransformationProjectArtifactDescriptor": {
+ "type": "structure",
+ "members": {
+ "sourceCodeArtifact": { "shape": "TransformationSourceCodeArtifactDescriptor" }
+ },
+ "union": true
},
"TransformationProjectState": {
"type": "structure",
"members": {
"language": { "shape": "TransformationLanguage" },
"runtimeEnv": { "shape": "TransformationRuntimeEnv" },
- "platformConfig": { "shape": "TransformationPlatformConfig" }
+ "platformConfig": { "shape": "TransformationPlatformConfig" },
+ "projectArtifact": { "shape": "TransformationProjectArtifactDescriptor" }
}
},
"TransformationRuntimeEnv": {
"type": "structure",
"members": {
"java": { "shape": "TransformationJavaRuntimeEnv" },
- "dotNet": { "shape": "TransformationDotNetRuntimeEnv" }
+ "dotNet": { "shape": "TransformationDotNetRuntimeEnv" },
+ "mainframe": { "shape": "TransformationMainframeRuntimeEnv" }
},
"union": true
},
+ "TransformationSourceCodeArtifactDescriptor": {
+ "type": "structure",
+ "members": {
+ "languages": { "shape": "TransformationLanguages" },
+ "runtimeEnv": { "shape": "TransformationRuntimeEnv" }
+ }
+ },
"TransformationSpec": {
"type": "structure",
"members": {
@@ -1912,11 +2006,11 @@
},
"TransformationType": {
"type": "string",
- "enum": ["LANGUAGE_UPGRADE"]
+ "enum": ["LANGUAGE_UPGRADE", "DOCUMENT_GENERATION"]
},
"TransformationUploadArtifactType": {
"type": "string",
- "enum": ["Dependencies"]
+ "enum": ["Dependencies", "ClientBuildResult"]
},
"TransformationUploadContext": {
"type": "structure",
@@ -1998,7 +2092,9 @@
"gitState": { "shape": "GitState" },
"envState": { "shape": "EnvState" },
"appStudioContext": { "shape": "AppStudioState" },
- "diagnostic": { "shape": "Diagnostic" }
+ "diagnostic": { "shape": "Diagnostic" },
+ "consoleState": { "shape": "ConsoleState" },
+ "userSettings": { "shape": "UserSettings" }
}
},
"UserIntent": {
@@ -2026,6 +2122,12 @@
"timestamp": { "shape": "Timestamp" }
}
},
+ "UserSettings": {
+ "type": "structure",
+ "members": {
+ "hasConsentedToCrossRegionCalls": { "shape": "Boolean" }
+ }
+ },
"UserTriggerDecisionEvent": {
"type": "structure",
"required": [
diff --git a/packages/core/src/amazonqFeatureDev/client/featureDev.ts b/packages/core/src/amazonqFeatureDev/client/featureDev.ts
index 9cb541ee357..869c240b323 100644
--- a/packages/core/src/amazonqFeatureDev/client/featureDev.ts
+++ b/packages/core/src/amazonqFeatureDev/client/featureDev.ts
@@ -10,7 +10,6 @@ import { ServiceOptions } from '../../shared/awsClientBuilder'
import globals from '../../shared/extensionGlobals'
import { getLogger } from '../../shared/logger'
import * as FeatureDevProxyClient from './featuredevproxyclient'
-import apiConfig = require('./codewhispererruntime-2022-11-11.json')
import { featureName } from '../constants'
import { CodeReference } from '../../amazonq/webview/ui/connector'
import {
@@ -25,6 +24,7 @@ import { getCodewhispererConfig } from '../../codewhisperer/client/codewhisperer
import { createCodeWhispererChatStreamingClient } from '../../shared/clients/codewhispererChatClient'
import { getClientId, getOptOutPreference, getOperatingSystem } from '../../shared/telemetry/util'
import { extensionVersion } from '../../shared/vscode/env'
+import apiConfig = require('./codewhispererruntime-2022-11-11.json')
// Create a client for featureDev proxy client based off of aws sdk v2
export async function createFeatureDevProxyClient(): Promise {
@@ -117,7 +117,12 @@ export class FeatureDevClient {
}
}
- public async startCodeGeneration(conversationId: string, uploadId: string, message: string) {
+ public async startCodeGeneration(
+ conversationId: string,
+ uploadId: string,
+ message: string,
+ currentCodeGenerationId?: string
+ ) {
try {
const client = await this.getClient()
const params = {
@@ -132,6 +137,9 @@ export class FeatureDevClient {
uploadId,
programmingLanguage: { languageName: 'javascript' },
},
+ } as FeatureDevProxyClient.Types.StartTaskAssistCodeGenerationRequest
+ if (currentCodeGenerationId) {
+ params.currentCodeGenerationId = currentCodeGenerationId
}
getLogger().debug(`Executing startTaskAssistCodeGeneration with %O`, params)
const response = await client.startTaskAssistCodeGeneration(params).promise()
diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
index fd3cb7ec736..8da49a3f095 100644
--- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
+++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
@@ -351,8 +351,11 @@ export class FeatureDevController {
const deletedFiles = session.state.deletedFiles ?? []
// Only add the follow up accept/deny buttons when the tab hasn't been closed/request hasn't been cancelled
- if (session?.state.tokenSource.token.isCancellationRequested) {
- this.workOnNewTask(message)
+ if (session?.state?.tokenSource?.token.isCancellationRequested) {
+ session?.state.tokenSource?.dispose()
+ if (session?.state?.tokenSource) {
+ session.state.tokenSource = undefined
+ }
return
}
@@ -703,11 +706,9 @@ export class FeatureDevController {
type: 'answer-part',
tabID: message.tabID,
})
-
- const session = await this.sessionStorage.getSession(message.tabID)
- session.state.tokenSource.cancel()
-
this.workOnNewTask(message)
+ const session = await this.sessionStorage.getSession(message.tabID)
+ session.state?.tokenSource?.cancel()
}
private async tabOpened(message: any) {
diff --git a/packages/core/src/amazonqFeatureDev/session/session.ts b/packages/core/src/amazonqFeatureDev/session/session.ts
index aec75383a11..21ed509b0a5 100644
--- a/packages/core/src/amazonqFeatureDev/session/session.ts
+++ b/packages/core/src/amazonqFeatureDev/session/session.ts
@@ -109,6 +109,7 @@ export class Session {
workspaceFolders: this.config.workspaceFolders,
proxyClient: this.proxyClient,
conversationId: this.conversationId,
+ currentCodeGenerationId: this.currentCodeGenerationId as string,
}
}
@@ -135,7 +136,7 @@ export class Session {
if (resp.nextState) {
// Cancel the request before moving to a new state
- this.state.tokenSource.cancel()
+ this.state?.tokenSource?.cancel()
// Move to the next state
this._state = resp.nextState
@@ -182,6 +183,10 @@ export class Session {
return this._state
}
+ get currentCodeGenerationId() {
+ return this.state.currentCodeGenerationId
+ }
+
get uploadId() {
if (!('uploadId' in this.state)) {
throw new Error("UploadId has to be initialized before it's read")
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index 3f80699704e..d1e3e3b7c7c 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -122,17 +122,21 @@ abstract class CodeGenBase {
private pollCount = 180
private requestDelay = 10000
public tokenSource: vscode.CancellationTokenSource
+ public isCancellationRequested: boolean
public phase: SessionStatePhase = DevPhase.CODEGEN
public readonly conversationId: string
public readonly uploadId: string
+ public readonly currentCodeGenerationId?: string
constructor(
protected config: SessionStateConfig,
public tabID: string
) {
this.tokenSource = new vscode.CancellationTokenSource()
+ this.isCancellationRequested = false
this.conversationId = config.conversationId
this.uploadId = config.uploadId
+ this.currentCodeGenerationId = config.currentCodeGenerationId
}
async generateCode({
@@ -156,7 +160,8 @@ abstract class CodeGenBase {
}> {
for (
let pollingIteration = 0;
- pollingIteration < this.pollCount && !this.tokenSource.token.isCancellationRequested;
+ pollingIteration < this.pollCount &&
+ (!this.isCancellationRequested || !this.tokenSource.token.isCancellationRequested);
++pollingIteration
) {
const codegenResult = await this.config.proxyClient.getCodeGeneration(this.conversationId, codeGenerationId)
@@ -232,7 +237,7 @@ abstract class CodeGenBase {
}
}
}
- if (!this.tokenSource.token.isCancellationRequested) {
+ if (!this.isCancellationRequested) {
// still in progress
const errorMessage = i18n('AWS.amazonq.featureDev.error.codeGen.timeout')
throw new ToolkitError(errorMessage, { code: 'CodeGenTimeout' })
@@ -268,7 +273,9 @@ export class CodeGenState extends CodeGenBase implements SessionState {
})
action.tokenSource?.token.onCancellationRequested(() => {
- if (action.tokenSource) this.tokenSource = action.tokenSource
+ this.isCancellationRequested = true
+ action.tokenSource?.dispose()
+ action.tokenSource = undefined
})
action.telemetry.setGenerateCodeIteration(this.currentIteration)
@@ -277,10 +284,11 @@ export class CodeGenState extends CodeGenBase implements SessionState {
const { codeGenerationId } = await this.config.proxyClient.startCodeGeneration(
this.config.conversationId,
this.config.uploadId,
- action.msg
+ action.msg,
+ this.config.currentCodeGenerationId
)
- if (!this.tokenSource.token.isCancellationRequested) {
+ if (!this.isCancellationRequested) {
action.messenger.sendAnswer({
message: i18n('AWS.amazonq.featureDev.pillText.generatingCode'),
type: 'answer-part',
@@ -420,6 +428,7 @@ export class MockCodeGenState implements SessionState {
export class PrepareCodeGenState implements SessionState {
public readonly phase = DevPhase.CODEGEN
public uploadId: string
+ public currentCodeGenerationId?: string
public conversationId: string
public tokenSource: vscode.CancellationTokenSource
constructor(
@@ -435,6 +444,7 @@ export class PrepareCodeGenState implements SessionState {
) {
this.tokenSource = superTokenSource || new vscode.CancellationTokenSource()
this.uploadId = config.uploadId
+ this.currentCodeGenerationId = config.currentCodeGenerationId
this.conversationId = config.conversationId
}
@@ -450,42 +460,43 @@ export class PrepareCodeGenState implements SessionState {
})
action.messenger.sendUpdatePlaceholder(this.tabID, i18n('AWS.amazonq.featureDev.pillText.uploadingCode'))
+ if (!this.uploadId) {
+ const uploadId = await telemetry.amazonq_createUpload.run(async (span) => {
+ span.record({
+ amazonqConversationId: this.config.conversationId,
+ credentialStartUrl: AuthUtil.instance.startUrl,
+ })
+ const { zipFileBuffer, zipFileChecksum } = await prepareRepoData(
+ this.config.workspaceRoots,
+ this.config.workspaceFolders,
+ action.telemetry,
+ span
+ )
- const uploadId = await telemetry.amazonq_createUpload.run(async (span) => {
- span.record({
- amazonqConversationId: this.config.conversationId,
- credentialStartUrl: AuthUtil.instance.startUrl,
- })
- const { zipFileBuffer, zipFileChecksum } = await prepareRepoData(
- this.config.workspaceRoots,
- this.config.workspaceFolders,
- action.telemetry,
- span
- )
-
- const { uploadUrl, uploadId, kmsKeyArn } = await this.config.proxyClient.createUploadUrl(
- this.config.conversationId,
- zipFileChecksum,
- zipFileBuffer.length
- )
+ const { uploadUrl, uploadId, kmsKeyArn } = await this.config.proxyClient.createUploadUrl(
+ this.config.conversationId,
+ zipFileChecksum,
+ zipFileBuffer.length
+ )
- await uploadCode(uploadUrl, zipFileBuffer, zipFileChecksum, kmsKeyArn)
- action.messenger.sendAnswer({
- message: i18n('AWS.amazonq.featureDev.pillText.contextGatheringCompleted'),
- type: 'answer-part',
- tabID: this.tabID,
- })
+ await uploadCode(uploadUrl, zipFileBuffer, zipFileChecksum, kmsKeyArn)
+ action.messenger.sendAnswer({
+ message: i18n('AWS.amazonq.featureDev.pillText.contextGatheringCompleted'),
+ type: 'answer-part',
+ tabID: this.tabID,
+ })
- action.messenger.sendUpdatePlaceholder(
- this.tabID,
- i18n('AWS.amazonq.featureDev.pillText.contextGatheringCompleted')
- )
+ action.messenger.sendUpdatePlaceholder(
+ this.tabID,
+ i18n('AWS.amazonq.featureDev.pillText.contextGatheringCompleted')
+ )
- return uploadId
- })
- this.uploadId = uploadId
+ return uploadId
+ })
+ this.uploadId = uploadId
+ }
const nextState = new CodeGenState(
- { ...this.config, uploadId },
+ { ...this.config, uploadId: this.uploadId, currentCodeGenerationId: this.config.currentCodeGenerationId },
this.filePaths,
this.deletedFiles,
this.references,
diff --git a/packages/core/src/amazonqFeatureDev/types.ts b/packages/core/src/amazonqFeatureDev/types.ts
index 21eb67ac7f0..4be827b12b8 100644
--- a/packages/core/src/amazonqFeatureDev/types.ts
+++ b/packages/core/src/amazonqFeatureDev/types.ts
@@ -60,7 +60,9 @@ export interface SessionState {
readonly references?: CodeReference[]
readonly phase?: SessionStatePhase
readonly uploadId: string
- readonly tokenSource: CancellationTokenSource
+ readonly currentCodeGenerationId?: string
+ approach: string
+ tokenSource?: CancellationTokenSource
readonly tabID: string
interact(action: SessionStateAction): Promise
updateWorkspaceRoot?: (workspaceRoot: string) => void
@@ -74,6 +76,7 @@ export interface SessionStateConfig {
conversationId: string
proxyClient: FeatureDevClient
uploadId: string
+ currentCodeGenerationId?: string
}
export interface SessionStateAction {
From 709c0c5e0fcc2e025059d58c46a89615e7dffb09 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Wed, 11 Sep 2024 15:06:56 -0400
Subject: [PATCH 05/87] fix(amazonqFeatureDev): fix merge conflicts
---
packages/core/src/amazonqFeatureDev/types.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/packages/core/src/amazonqFeatureDev/types.ts b/packages/core/src/amazonqFeatureDev/types.ts
index 4be827b12b8..97b52b1b6e2 100644
--- a/packages/core/src/amazonqFeatureDev/types.ts
+++ b/packages/core/src/amazonqFeatureDev/types.ts
@@ -61,7 +61,6 @@ export interface SessionState {
readonly phase?: SessionStatePhase
readonly uploadId: string
readonly currentCodeGenerationId?: string
- approach: string
tokenSource?: CancellationTokenSource
readonly tabID: string
interact(action: SessionStateAction): Promise
From 644dedd6bfff9897f9640f05a6e53cec464a21e2 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Thu, 12 Sep 2024 10:52:20 -0400
Subject: [PATCH 06/87] fix(amazonqFeatureDev): include currentCodeGenerationId
to track reference for stop code generation
---
.../controllers/chat/controller.ts | 13 ++++++++-----
.../src/amazonqFeatureDev/session/session.ts | 2 +-
.../amazonqFeatureDev/session/sessionState.ts | 19 ++++++++++++-------
packages/core/src/amazonqFeatureDev/types.ts | 3 ++-
.../session/sessionState.test.ts | 2 --
5 files changed, 23 insertions(+), 16 deletions(-)
diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
index 8da49a3f095..debbd8d2d99 100644
--- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
+++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
@@ -349,13 +349,8 @@ export class FeatureDevController {
await session.send(message)
const filePaths = session.state.filePaths ?? []
const deletedFiles = session.state.deletedFiles ?? []
-
// Only add the follow up accept/deny buttons when the tab hasn't been closed/request hasn't been cancelled
if (session?.state?.tokenSource?.token.isCancellationRequested) {
- session?.state.tokenSource?.dispose()
- if (session?.state?.tokenSource) {
- session.state.tokenSource = undefined
- }
return
}
@@ -416,6 +411,14 @@ export class FeatureDevController {
this.messenger.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.featureDev.pillText.selectOption'))
} finally {
// Finish processing the event
+
+ if (session?.state?.tokenSource?.token.isCancellationRequested) {
+ session?.state.tokenSource?.dispose()
+ if (session?.state?.tokenSource) {
+ session.state.tokenSource = undefined
+ }
+ return
+ }
this.messenger.sendAsyncEventProgress(tabID, false, undefined)
// Lock the chat input until they explicitly click one of the follow ups
diff --git a/packages/core/src/amazonqFeatureDev/session/session.ts b/packages/core/src/amazonqFeatureDev/session/session.ts
index 21ed509b0a5..448156c63eb 100644
--- a/packages/core/src/amazonqFeatureDev/session/session.ts
+++ b/packages/core/src/amazonqFeatureDev/session/session.ts
@@ -89,6 +89,7 @@ export class Session {
...this.getSessionStateConfig(),
conversationId: this.conversationId,
uploadId: '',
+ currentCodeGenerationId: undefined,
},
[],
[],
@@ -109,7 +110,6 @@ export class Session {
workspaceFolders: this.config.workspaceFolders,
proxyClient: this.proxyClient,
conversationId: this.conversationId,
- currentCodeGenerationId: this.currentCodeGenerationId as string,
}
}
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index d1e3e3b7c7c..86a4a117d52 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -126,7 +126,7 @@ abstract class CodeGenBase {
public phase: SessionStatePhase = DevPhase.CODEGEN
public readonly conversationId: string
public readonly uploadId: string
- public readonly currentCodeGenerationId?: string
+ public currentCodeGenerationId?: string
constructor(
protected config: SessionStateConfig,
@@ -274,6 +274,7 @@ export class CodeGenState extends CodeGenBase implements SessionState {
action.tokenSource?.token.onCancellationRequested(() => {
this.isCancellationRequested = true
+ if (action.tokenSource) this.tokenSource = action.tokenSource
action.tokenSource?.dispose()
action.tokenSource = undefined
})
@@ -285,9 +286,12 @@ export class CodeGenState extends CodeGenBase implements SessionState {
this.config.conversationId,
this.config.uploadId,
action.msg,
- this.config.currentCodeGenerationId
+ this.currentCodeGenerationId
)
+ this.currentCodeGenerationId = codeGenerationId
+ this.config.currentCodeGenerationId = codeGenerationId
+
if (!this.isCancellationRequested) {
action.messenger.sendAnswer({
message: i18n('AWS.amazonq.featureDev.pillText.generatingCode'),
@@ -325,7 +329,8 @@ export class CodeGenState extends CodeGenBase implements SessionState {
this.currentIteration + 1,
this.codeGenerationRemainingIterationCount,
this.codeGenerationTotalIterationCount,
- this.tokenSource
+ this.tokenSource,
+ this.currentCodeGenerationId
)
return {
nextState,
@@ -428,7 +433,6 @@ export class MockCodeGenState implements SessionState {
export class PrepareCodeGenState implements SessionState {
public readonly phase = DevPhase.CODEGEN
public uploadId: string
- public currentCodeGenerationId?: string
public conversationId: string
public tokenSource: vscode.CancellationTokenSource
constructor(
@@ -440,11 +444,12 @@ export class PrepareCodeGenState implements SessionState {
private currentIteration: number,
public codeGenerationRemainingIterationCount?: number,
public codeGenerationTotalIterationCount?: number,
- public superTokenSource?: vscode.CancellationTokenSource
+ public superTokenSource?: vscode.CancellationTokenSource,
+ public currentCodeGenerationId?: string
) {
this.tokenSource = superTokenSource || new vscode.CancellationTokenSource()
this.uploadId = config.uploadId
- this.currentCodeGenerationId = config.currentCodeGenerationId
+ this.currentCodeGenerationId = currentCodeGenerationId
this.conversationId = config.conversationId
}
@@ -496,7 +501,7 @@ export class PrepareCodeGenState implements SessionState {
this.uploadId = uploadId
}
const nextState = new CodeGenState(
- { ...this.config, uploadId: this.uploadId, currentCodeGenerationId: this.config.currentCodeGenerationId },
+ { ...this.config, uploadId: this.uploadId, currentCodeGenerationId: this.currentCodeGenerationId },
this.filePaths,
this.deletedFiles,
this.references,
diff --git a/packages/core/src/amazonqFeatureDev/types.ts b/packages/core/src/amazonqFeatureDev/types.ts
index 97b52b1b6e2..48e588fc371 100644
--- a/packages/core/src/amazonqFeatureDev/types.ts
+++ b/packages/core/src/amazonqFeatureDev/types.ts
@@ -21,6 +21,7 @@ export type Interaction = {
export interface SessionStateInteraction {
nextState: SessionState | Omit | undefined
interaction: Interaction
+ currentCodeGenerationId?: string
}
export enum DevPhase {
@@ -60,7 +61,7 @@ export interface SessionState {
readonly references?: CodeReference[]
readonly phase?: SessionStatePhase
readonly uploadId: string
- readonly currentCodeGenerationId?: string
+ currentCodeGenerationId?: string
tokenSource?: CancellationTokenSource
readonly tabID: string
interact(action: SessionStateAction): Promise
diff --git a/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts b/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts
index 91833383073..d07f8dbbc23 100644
--- a/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts
+++ b/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts
@@ -30,7 +30,6 @@ const mockSessionStateAction = (msg?: string): SessionStateAction => {
}
}
-let mockGeneratePlan: sinon.SinonStub
let mockGetCodeGeneration: sinon.SinonStub
let mockExportResultArchive: sinon.SinonStub
let mockCreateUploadUrl: sinon.SinonStub
@@ -49,7 +48,6 @@ const mockSessionStateConfig = ({
proxyClient: {
createConversation: () => sinon.stub(),
createUploadUrl: () => mockCreateUploadUrl(),
- generatePlan: () => mockGeneratePlan(),
startCodeGeneration: () => sinon.stub(),
getCodeGeneration: () => mockGetCodeGeneration(),
exportResultArchive: () => mockExportResultArchive(),
From ba2db03ee2daa7510ce4c31483067dc5ed722236 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Thu, 12 Sep 2024 14:42:05 -0400
Subject: [PATCH 07/87] refactor(amazonqFeatureDev): use else catch for return
statement
---
.../controllers/chat/controller.ts | 29 ++++++++++---------
1 file changed, 15 insertions(+), 14 deletions(-)
diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
index debbd8d2d99..cbba6381043 100644
--- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
+++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
@@ -417,22 +417,23 @@ export class FeatureDevController {
if (session?.state?.tokenSource) {
session.state.tokenSource = undefined
}
- return
- }
- this.messenger.sendAsyncEventProgress(tabID, false, undefined)
+ getLogger().debug('Request cancelled, skipping further processing')
+ } else {
+ this.messenger.sendAsyncEventProgress(tabID, false, undefined)
- // Lock the chat input until they explicitly click one of the follow ups
- this.messenger.sendChatInputEnabled(tabID, false)
+ // Lock the chat input until they explicitly click one of the follow ups
+ this.messenger.sendChatInputEnabled(tabID, false)
- if (!this.isAmazonQVisible) {
- const open = 'Open chat'
- const resp = await vscode.window.showInformationMessage(
- i18n('AWS.amazonq.featureDev.answer.qGeneratedCode'),
- open
- )
- if (resp === open) {
- await vscode.commands.executeCommand('aws.AmazonQChatView.focus')
- // TODO add focusing on the specific tab once that's implemented
+ if (!this.isAmazonQVisible) {
+ const open = 'Open chat'
+ const resp = await vscode.window.showInformationMessage(
+ i18n('AWS.amazonq.featureDev.answer.qGeneratedCode'),
+ open
+ )
+ if (resp === open) {
+ await vscode.commands.executeCommand('aws.AmazonQChatView.focus')
+ // TODO add focusing on the specific tab once that's implemented
+ }
}
}
}
From b791a2bc13c890f35580550a148ad8cbd9ba3ecd Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Tue, 17 Sep 2024 13:12:01 -0400
Subject: [PATCH 08/87] refactor(amazonqFeatureDev): add logic for
codeGenerationId
---
.../codewhispererruntime-2022-11-11.json | 7 +-
.../amazonqFeatureDev/client/featureDev.ts | 2 +
.../amazonqFeatureDev/session/sessionState.ts | 66 +++++++++----------
3 files changed, 40 insertions(+), 35 deletions(-)
diff --git a/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json b/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json
index b487286e0ca..e6e226f4976 100644
--- a/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json
+++ b/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json
@@ -439,7 +439,8 @@
"acceptedCharacterCount": { "shape": "Integer" },
"acceptedLineCount": { "shape": "Integer" },
"acceptedSnippetHasReference": { "shape": "Boolean" },
- "hasProjectLevelContext": { "shape": "Boolean" }
+ "hasProjectLevelContext": { "shape": "Boolean" },
+ "userIntent": { "shape": "UserIntent" }
}
},
"ChatInteractWithMessageEventInteractionTargetString": {
@@ -1564,6 +1565,7 @@
"conversationState": { "shape": "ConversationState" },
"workspaceState": { "shape": "WorkspaceState" },
"taskAssistPlan": { "shape": "TaskAssistPlan" },
+ "codeGenerationId": { "shape": "CodeGenerationId" },
"currentCodeGenerationId": { "shape": "CodeGenerationId" }
}
},
@@ -2107,7 +2109,8 @@
"CITE_SOURCES",
"EXPLAIN_LINE_BY_LINE",
"EXPLAIN_CODE_SELECTION",
- "GENERATE_CLOUDFORMATION_TEMPLATE"
+ "GENERATE_CLOUDFORMATION_TEMPLATE",
+ "GENERATE_UNIT_TESTS"
]
},
"UserModificationEvent": {
diff --git a/packages/core/src/amazonqFeatureDev/client/featureDev.ts b/packages/core/src/amazonqFeatureDev/client/featureDev.ts
index 869c240b323..9eb7c38e54f 100644
--- a/packages/core/src/amazonqFeatureDev/client/featureDev.ts
+++ b/packages/core/src/amazonqFeatureDev/client/featureDev.ts
@@ -121,11 +121,13 @@ export class FeatureDevClient {
conversationId: string,
uploadId: string,
message: string,
+ codeGenerationId: string,
currentCodeGenerationId?: string
) {
try {
const client = await this.getClient()
const params = {
+ codeGenerationId,
conversationState: {
conversationId,
currentMessage: {
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index 86a4a117d52..7453d5e63ce 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -281,11 +281,12 @@ export class CodeGenState extends CodeGenBase implements SessionState {
action.telemetry.setGenerateCodeIteration(this.currentIteration)
action.telemetry.setGenerateCodeLastInvocationTime()
-
- const { codeGenerationId } = await this.config.proxyClient.startCodeGeneration(
+ const codeGenerationId = randomUUID()
+ await this.config.proxyClient.startCodeGeneration(
this.config.conversationId,
this.config.uploadId,
action.msg,
+ codeGenerationId,
this.currentCodeGenerationId
)
@@ -465,41 +466,40 @@ export class PrepareCodeGenState implements SessionState {
})
action.messenger.sendUpdatePlaceholder(this.tabID, i18n('AWS.amazonq.featureDev.pillText.uploadingCode'))
- if (!this.uploadId) {
- const uploadId = await telemetry.amazonq_createUpload.run(async (span) => {
- span.record({
- amazonqConversationId: this.config.conversationId,
- credentialStartUrl: AuthUtil.instance.startUrl,
- })
- const { zipFileBuffer, zipFileChecksum } = await prepareRepoData(
- this.config.workspaceRoots,
- this.config.workspaceFolders,
- action.telemetry,
- span
- )
+ const uploadId = await telemetry.amazonq_createUpload.run(async (span) => {
+ span.record({
+ amazonqConversationId: this.config.conversationId,
+ credentialStartUrl: AuthUtil.instance.startUrl,
+ })
+ const { zipFileBuffer, zipFileChecksum } = await prepareRepoData(
+ this.config.workspaceRoots,
+ this.config.workspaceFolders,
+ action.telemetry,
+ span
+ )
- const { uploadUrl, uploadId, kmsKeyArn } = await this.config.proxyClient.createUploadUrl(
- this.config.conversationId,
- zipFileChecksum,
- zipFileBuffer.length
- )
+ const { uploadUrl, uploadId, kmsKeyArn } = await this.config.proxyClient.createUploadUrl(
+ this.config.conversationId,
+ zipFileChecksum,
+ zipFileBuffer.length
+ )
- await uploadCode(uploadUrl, zipFileBuffer, zipFileChecksum, kmsKeyArn)
- action.messenger.sendAnswer({
- message: i18n('AWS.amazonq.featureDev.pillText.contextGatheringCompleted'),
- type: 'answer-part',
- tabID: this.tabID,
- })
+ await uploadCode(uploadUrl, zipFileBuffer, zipFileChecksum, kmsKeyArn)
+ action.messenger.sendAnswer({
+ message: i18n('AWS.amazonq.featureDev.pillText.contextGatheringCompleted'),
+ type: 'answer-part',
+ tabID: this.tabID,
+ })
- action.messenger.sendUpdatePlaceholder(
- this.tabID,
- i18n('AWS.amazonq.featureDev.pillText.contextGatheringCompleted')
- )
+ action.messenger.sendUpdatePlaceholder(
+ this.tabID,
+ i18n('AWS.amazonq.featureDev.pillText.contextGatheringCompleted')
+ )
+
+ return uploadId
+ })
+ this.uploadId = uploadId
- return uploadId
- })
- this.uploadId = uploadId
- }
const nextState = new CodeGenState(
{ ...this.config, uploadId: this.uploadId, currentCodeGenerationId: this.currentCodeGenerationId },
this.filePaths,
From e1cbe15d0341370a909af9391ce430354e54b755 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Tue, 17 Sep 2024 15:43:48 -0400
Subject: [PATCH 09/87] fix(amazonqFeatureDev): generate uploadId in fe
---
.../client/codewhispererruntime-2022-11-11.json | 3 ++-
.../core/src/amazonqFeatureDev/client/featureDev.ts | 10 ++++++++--
.../core/src/amazonqFeatureDev/session/sessionState.ts | 7 ++++---
3 files changed, 14 insertions(+), 6 deletions(-)
diff --git a/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json b/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json
index e6e226f4976..79b9a0f66fd 100644
--- a/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json
+++ b/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json
@@ -669,7 +669,8 @@
"contentLength": { "shape": "CreateUploadUrlRequestContentLengthLong" },
"artifactType": { "shape": "ArtifactType" },
"uploadIntent": { "shape": "UploadIntent" },
- "uploadContext": { "shape": "UploadContext" }
+ "uploadContext": { "shape": "UploadContext" },
+ "uploadId": { "shape": "UploadId" }
}
},
"CreateUploadUrlRequestContentChecksumString": {
diff --git a/packages/core/src/amazonqFeatureDev/client/featureDev.ts b/packages/core/src/amazonqFeatureDev/client/featureDev.ts
index 9eb7c38e54f..b8473e6103b 100644
--- a/packages/core/src/amazonqFeatureDev/client/featureDev.ts
+++ b/packages/core/src/amazonqFeatureDev/client/featureDev.ts
@@ -80,7 +80,12 @@ export class FeatureDevClient {
}
}
- public async createUploadUrl(conversationId: string, contentChecksumSha256: string, contentLength: number) {
+ public async createUploadUrl(
+ conversationId: string,
+ contentChecksumSha256: string,
+ contentLength: number,
+ uploadId: string
+ ) {
try {
const client = await this.getClient()
const params = {
@@ -89,6 +94,7 @@ export class FeatureDevClient {
conversationId,
},
},
+ uploadId,
contentChecksum: contentChecksumSha256,
contentChecksumType: 'SHA_256',
artifactType: 'SourceCode',
@@ -98,7 +104,7 @@ export class FeatureDevClient {
getLogger().debug(`Executing createUploadUrl with %O`, omit(params, 'contentChecksum'))
const response = await client.createUploadUrl(params).promise()
getLogger().debug(`${featureName}: Created upload url: %O`, {
- uploadId: response.uploadId,
+ uploadId: uploadId,
requestId: response.$response.requestId,
})
return response
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index 7453d5e63ce..145acfc035c 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -477,11 +477,12 @@ export class PrepareCodeGenState implements SessionState {
action.telemetry,
span
)
-
- const { uploadUrl, uploadId, kmsKeyArn } = await this.config.proxyClient.createUploadUrl(
+ const uploadId = randomUUID()
+ const { uploadUrl, kmsKeyArn } = await this.config.proxyClient.createUploadUrl(
this.config.conversationId,
zipFileChecksum,
- zipFileBuffer.length
+ zipFileBuffer.length,
+ uploadId
)
await uploadCode(uploadUrl, zipFileBuffer, zipFileChecksum, kmsKeyArn)
From ae9d16b2588842d2faddf98fcc9fc0831529e915 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Wed, 25 Sep 2024 09:53:40 -0400
Subject: [PATCH 10/87] fix(amazonqFeatureDev): create a new token to stop and
iterate in the same session
---
.../controllers/chat/controller.ts | 6 +++--
.../src/amazonqFeatureDev/session/session.ts | 3 +--
.../amazonqFeatureDev/session/sessionState.ts | 23 +++++++++++--------
3 files changed, 19 insertions(+), 13 deletions(-)
diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
index cbba6381043..c5876c4efca 100644
--- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
+++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
@@ -415,7 +415,7 @@ export class FeatureDevController {
if (session?.state?.tokenSource?.token.isCancellationRequested) {
session?.state.tokenSource?.dispose()
if (session?.state?.tokenSource) {
- session.state.tokenSource = undefined
+ session.state.tokenSource = new vscode.CancellationTokenSource()
}
getLogger().debug('Request cancelled, skipping further processing')
} else {
@@ -712,7 +712,9 @@ export class FeatureDevController {
})
this.workOnNewTask(message)
const session = await this.sessionStorage.getSession(message.tabID)
- session.state?.tokenSource?.cancel()
+ if (session.state?.tokenSource) {
+ session.state?.tokenSource?.cancel()
+ }
}
private async tabOpened(message: any) {
diff --git a/packages/core/src/amazonqFeatureDev/session/session.ts b/packages/core/src/amazonqFeatureDev/session/session.ts
index 448156c63eb..cef143eb7a0 100644
--- a/packages/core/src/amazonqFeatureDev/session/session.ts
+++ b/packages/core/src/amazonqFeatureDev/session/session.ts
@@ -26,7 +26,6 @@ import { ReferenceLogViewProvider } from '../../codewhisperer/service/referenceL
import { AuthUtil } from '../../codewhisperer/util/authUtil'
import { getLogger } from '../../shared'
import { logWithConversationId } from '../userFacingText'
-
export class Session {
private _state?: SessionState | Omit
private task: string = ''
@@ -136,7 +135,7 @@ export class Session {
if (resp.nextState) {
// Cancel the request before moving to a new state
- this.state?.tokenSource?.cancel()
+ if (!this.state?.tokenSource?.token.isCancellationRequested) this.state?.tokenSource?.cancel()
// Move to the next state
this._state = resp.nextState
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index 145acfc035c..67c4e45eb56 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -39,6 +39,8 @@ import { collectFiles, getWorkspaceFoldersByPrefixes } from '../../shared/utilit
import { i18n } from '../../shared/i18n-helper'
import { Messenger } from '../controllers/chat/messenger/messenger'
+const EMPTY_CODEGEN_ID = 'EMPTY_CURRENT_CODE_GENERATION_ID'
+
export class ConversationNotStartedState implements Omit {
public tokenSource: vscode.CancellationTokenSource
public readonly phase = DevPhase.INIT
@@ -136,7 +138,7 @@ abstract class CodeGenBase {
this.isCancellationRequested = false
this.conversationId = config.conversationId
this.uploadId = config.uploadId
- this.currentCodeGenerationId = config.currentCodeGenerationId
+ this.currentCodeGenerationId = config.currentCodeGenerationId || EMPTY_CODEGEN_ID
}
async generateCode({
@@ -160,8 +162,7 @@ abstract class CodeGenBase {
}> {
for (
let pollingIteration = 0;
- pollingIteration < this.pollCount &&
- (!this.isCancellationRequested || !this.tokenSource.token.isCancellationRequested);
+ pollingIteration < this.pollCount && !this.isCancellationRequested;
++pollingIteration
) {
const codegenResult = await this.config.proxyClient.getCodeGeneration(this.conversationId, codeGenerationId)
@@ -275,8 +276,6 @@ export class CodeGenState extends CodeGenBase implements SessionState {
action.tokenSource?.token.onCancellationRequested(() => {
this.isCancellationRequested = true
if (action.tokenSource) this.tokenSource = action.tokenSource
- action.tokenSource?.dispose()
- action.tokenSource = undefined
})
action.telemetry.setGenerateCodeIteration(this.currentIteration)
@@ -290,9 +289,6 @@ export class CodeGenState extends CodeGenBase implements SessionState {
this.currentCodeGenerationId
)
- this.currentCodeGenerationId = codeGenerationId
- this.config.currentCodeGenerationId = codeGenerationId
-
if (!this.isCancellationRequested) {
action.messenger.sendAnswer({
message: i18n('AWS.amazonq.featureDev.pillText.generatingCode'),
@@ -313,6 +309,11 @@ export class CodeGenState extends CodeGenBase implements SessionState {
workspaceFolders: this.config.workspaceFolders,
})
+ if (codeGeneration && !this.isCancellationRequested) {
+ this.config.currentCodeGenerationId = codeGenerationId
+ this.currentCodeGenerationId = codeGenerationId
+ }
+
this.filePaths = codeGeneration.newFiles
this.deletedFiles = codeGeneration.deletedFiles
this.references = codeGeneration.references
@@ -478,7 +479,11 @@ export class PrepareCodeGenState implements SessionState {
span
)
const uploadId = randomUUID()
- const { uploadUrl, kmsKeyArn } = await this.config.proxyClient.createUploadUrl(
+ const {
+ uploadUrl,
+ uploadId: returnedUploadId,
+ kmsKeyArn,
+ } = await this.config.proxyClient.createUploadUrl(
this.config.conversationId,
zipFileChecksum,
zipFileBuffer.length,
From 532e4e4bd85cc1fccb9ca391d10211fc9d190eaa Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Wed, 25 Sep 2024 10:05:53 -0400
Subject: [PATCH 11/87] fix(amazonqFeatureDev): removeu unused const
---
packages/core/src/amazonqFeatureDev/session/sessionState.ts | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index 67c4e45eb56..f6757f3f818 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -479,11 +479,7 @@ export class PrepareCodeGenState implements SessionState {
span
)
const uploadId = randomUUID()
- const {
- uploadUrl,
- uploadId: returnedUploadId,
- kmsKeyArn,
- } = await this.config.proxyClient.createUploadUrl(
+ const { uploadUrl, kmsKeyArn } = await this.config.proxyClient.createUploadUrl(
this.config.conversationId,
zipFileChecksum,
zipFileBuffer.length,
From d63ceb5bb3a590dba71b1bca952fdbec086ede08 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Thu, 26 Sep 2024 13:47:42 -0400
Subject: [PATCH 12/87] fix(dev): apply fixes based on new eslint rules
---
packages/core/src/amazonqFeatureDev/session/session.ts | 4 +++-
.../core/src/amazonqFeatureDev/session/sessionState.ts | 8 +++++---
2 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/packages/core/src/amazonqFeatureDev/session/session.ts b/packages/core/src/amazonqFeatureDev/session/session.ts
index cef143eb7a0..76891db5c8a 100644
--- a/packages/core/src/amazonqFeatureDev/session/session.ts
+++ b/packages/core/src/amazonqFeatureDev/session/session.ts
@@ -135,7 +135,9 @@ export class Session {
if (resp.nextState) {
// Cancel the request before moving to a new state
- if (!this.state?.tokenSource?.token.isCancellationRequested) this.state?.tokenSource?.cancel()
+ if (!this.state?.tokenSource?.token.isCancellationRequested) {
+ this.state?.tokenSource?.cancel()
+ }
// Move to the next state
this._state = resp.nextState
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index f6757f3f818..d940495bf51 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -39,7 +39,7 @@ import { collectFiles, getWorkspaceFoldersByPrefixes } from '../../shared/utilit
import { i18n } from '../../shared/i18n-helper'
import { Messenger } from '../controllers/chat/messenger/messenger'
-const EMPTY_CODEGEN_ID = 'EMPTY_CURRENT_CODE_GENERATION_ID'
+const EmptyCodeGenID = 'EMPTY_CURRENT_CODE_GENERATION_ID'
export class ConversationNotStartedState implements Omit {
public tokenSource: vscode.CancellationTokenSource
@@ -138,7 +138,7 @@ abstract class CodeGenBase {
this.isCancellationRequested = false
this.conversationId = config.conversationId
this.uploadId = config.uploadId
- this.currentCodeGenerationId = config.currentCodeGenerationId || EMPTY_CODEGEN_ID
+ this.currentCodeGenerationId = config.currentCodeGenerationId || EmptyCodeGenID
}
async generateCode({
@@ -275,7 +275,9 @@ export class CodeGenState extends CodeGenBase implements SessionState {
action.tokenSource?.token.onCancellationRequested(() => {
this.isCancellationRequested = true
- if (action.tokenSource) this.tokenSource = action.tokenSource
+ if (action.tokenSource) {
+ this.tokenSource = action.tokenSource
+ }
})
action.telemetry.setGenerateCodeIteration(this.currentIteration)
From f0bbd9f26373af17814f9159fb347f297c55ac21 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Thu, 26 Sep 2024 15:12:15 -0400
Subject: [PATCH 13/87] fix(amazonqFeatureDev): use shared context from action
to check cancellation
---
.../controllers/chat/controller.ts | 17 +++++---
.../amazonqFeatureDev/session/sessionState.ts | 42 +++++++++----------
2 files changed, 31 insertions(+), 28 deletions(-)
diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
index c5876c4efca..fd83c2b6bdd 100644
--- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
+++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
@@ -284,6 +284,16 @@ export class FeatureDevController {
}
}
+ private disposeToken(session: Session | undefined) {
+ if (session?.state?.tokenSource?.token.isCancellationRequested) {
+ session?.state.tokenSource?.dispose()
+ if (session?.state?.tokenSource) {
+ session.state.tokenSource = new vscode.CancellationTokenSource()
+ }
+ getLogger().debug('Request cancelled, skipping further processing')
+ }
+ }
+
// TODO add type
private async processUserChatMessage(message: any) {
if (message.message === undefined) {
@@ -319,6 +329,7 @@ export class FeatureDevController {
await this.onCodeGeneration(session, message.message, message.tabID)
}
} catch (err: any) {
+ this.disposeToken(session)
this.processErrorChatMessage(err, message, session)
// Lock the chat input until they explicitly click one of the follow ups
this.messenger.sendChatInputEnabled(message.tabID, false)
@@ -413,11 +424,7 @@ export class FeatureDevController {
// Finish processing the event
if (session?.state?.tokenSource?.token.isCancellationRequested) {
- session?.state.tokenSource?.dispose()
- if (session?.state?.tokenSource) {
- session.state.tokenSource = new vscode.CancellationTokenSource()
- }
- getLogger().debug('Request cancelled, skipping further processing')
+ this.disposeToken(session)
} else {
this.messenger.sendAsyncEventProgress(tabID, false, undefined)
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index d940495bf51..04d07d5190d 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -124,7 +124,6 @@ abstract class CodeGenBase {
private pollCount = 180
private requestDelay = 10000
public tokenSource: vscode.CancellationTokenSource
- public isCancellationRequested: boolean
public phase: SessionStatePhase = DevPhase.CODEGEN
public readonly conversationId: string
public readonly uploadId: string
@@ -135,7 +134,6 @@ abstract class CodeGenBase {
public tabID: string
) {
this.tokenSource = new vscode.CancellationTokenSource()
- this.isCancellationRequested = false
this.conversationId = config.conversationId
this.uploadId = config.uploadId
this.currentCodeGenerationId = config.currentCodeGenerationId || EmptyCodeGenID
@@ -147,12 +145,14 @@ abstract class CodeGenBase {
codeGenerationId,
telemetry: telemetry,
workspaceFolders,
+ isCancellationRequested,
}: {
messenger: Messenger
fs: VirtualFileSystem
codeGenerationId: string
telemetry: TelemetryHelper
workspaceFolders: CurrentWsFolders
+ isCancellationRequested?: boolean
}): Promise<{
newFiles: NewFileInfo[]
deletedFiles: DeletedFileInfo[]
@@ -162,7 +162,7 @@ abstract class CodeGenBase {
}> {
for (
let pollingIteration = 0;
- pollingIteration < this.pollCount && !this.isCancellationRequested;
+ pollingIteration < this.pollCount && !isCancellationRequested;
++pollingIteration
) {
const codegenResult = await this.config.proxyClient.getCodeGeneration(this.conversationId, codeGenerationId)
@@ -238,7 +238,7 @@ abstract class CodeGenBase {
}
}
}
- if (!this.isCancellationRequested) {
+ if (!isCancellationRequested) {
// still in progress
const errorMessage = i18n('AWS.amazonq.featureDev.error.codeGen.timeout')
throw new ToolkitError(errorMessage, { code: 'CodeGenTimeout' })
@@ -273,13 +273,6 @@ export class CodeGenState extends CodeGenBase implements SessionState {
credentialStartUrl: AuthUtil.instance.startUrl,
})
- action.tokenSource?.token.onCancellationRequested(() => {
- this.isCancellationRequested = true
- if (action.tokenSource) {
- this.tokenSource = action.tokenSource
- }
- })
-
action.telemetry.setGenerateCodeIteration(this.currentIteration)
action.telemetry.setGenerateCodeLastInvocationTime()
const codeGenerationId = randomUUID()
@@ -291,7 +284,7 @@ export class CodeGenState extends CodeGenBase implements SessionState {
this.currentCodeGenerationId
)
- if (!this.isCancellationRequested) {
+ if (!action.tokenSource?.token.isCancellationRequested) {
action.messenger.sendAnswer({
message: i18n('AWS.amazonq.featureDev.pillText.generatingCode'),
type: 'answer-part',
@@ -309,9 +302,10 @@ export class CodeGenState extends CodeGenBase implements SessionState {
codeGenerationId,
telemetry: action.telemetry,
workspaceFolders: this.config.workspaceFolders,
+ isCancellationRequested: action.tokenSource?.token.isCancellationRequested,
})
- if (codeGeneration && !this.isCancellationRequested) {
+ if (codeGeneration && !action.tokenSource?.token.isCancellationRequested) {
this.config.currentCodeGenerationId = codeGenerationId
this.currentCodeGenerationId = codeGenerationId
}
@@ -333,7 +327,7 @@ export class CodeGenState extends CodeGenBase implements SessionState {
this.currentIteration + 1,
this.codeGenerationRemainingIterationCount,
this.codeGenerationTotalIterationCount,
- this.tokenSource,
+ action.tokenSource,
this.currentCodeGenerationId
)
return {
@@ -489,16 +483,18 @@ export class PrepareCodeGenState implements SessionState {
)
await uploadCode(uploadUrl, zipFileBuffer, zipFileChecksum, kmsKeyArn)
- action.messenger.sendAnswer({
- message: i18n('AWS.amazonq.featureDev.pillText.contextGatheringCompleted'),
- type: 'answer-part',
- tabID: this.tabID,
- })
+ if (!action.tokenSource?.token.isCancellationRequested) {
+ action.messenger.sendAnswer({
+ message: i18n('AWS.amazonq.featureDev.pillText.contextGatheringCompleted'),
+ type: 'answer-part',
+ tabID: this.tabID,
+ })
- action.messenger.sendUpdatePlaceholder(
- this.tabID,
- i18n('AWS.amazonq.featureDev.pillText.contextGatheringCompleted')
- )
+ action.messenger.sendUpdatePlaceholder(
+ this.tabID,
+ i18n('AWS.amazonq.featureDev.pillText.contextGatheringCompleted')
+ )
+ }
return uploadId
})
From e60467200c4b4a4c568d490d8c233e883f5b8ff6 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Thu, 26 Sep 2024 15:53:17 -0400
Subject: [PATCH 14/87] fix(amazonqFeatureDev): require isCancellation to
localize in the instance and abort the individual call
---
.../amazonqFeatureDev/session/sessionState.ts | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index 04d07d5190d..d983cba1cab 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -128,6 +128,7 @@ abstract class CodeGenBase {
public readonly conversationId: string
public readonly uploadId: string
public currentCodeGenerationId?: string
+ public isCancellationRequested?: boolean
constructor(
protected config: SessionStateConfig,
@@ -145,14 +146,12 @@ abstract class CodeGenBase {
codeGenerationId,
telemetry: telemetry,
workspaceFolders,
- isCancellationRequested,
}: {
messenger: Messenger
fs: VirtualFileSystem
codeGenerationId: string
telemetry: TelemetryHelper
workspaceFolders: CurrentWsFolders
- isCancellationRequested?: boolean
}): Promise<{
newFiles: NewFileInfo[]
deletedFiles: DeletedFileInfo[]
@@ -162,7 +161,7 @@ abstract class CodeGenBase {
}> {
for (
let pollingIteration = 0;
- pollingIteration < this.pollCount && !isCancellationRequested;
+ pollingIteration < this.pollCount && !this.isCancellationRequested;
++pollingIteration
) {
const codegenResult = await this.config.proxyClient.getCodeGeneration(this.conversationId, codeGenerationId)
@@ -238,7 +237,7 @@ abstract class CodeGenBase {
}
}
}
- if (!isCancellationRequested) {
+ if (!this.isCancellationRequested) {
// still in progress
const errorMessage = i18n('AWS.amazonq.featureDev.error.codeGen.timeout')
throw new ToolkitError(errorMessage, { code: 'CodeGenTimeout' })
@@ -268,6 +267,12 @@ export class CodeGenState extends CodeGenBase implements SessionState {
async interact(action: SessionStateAction): Promise {
return telemetry.amazonq_codeGenerationInvoke.run(async (span) => {
try {
+ action.tokenSource?.token.onCancellationRequested(() => {
+ this.isCancellationRequested = true
+ if (action.tokenSource) {
+ this.tokenSource = action.tokenSource
+ }
+ })
span.record({
amazonqConversationId: this.config.conversationId,
credentialStartUrl: AuthUtil.instance.startUrl,
@@ -284,7 +289,7 @@ export class CodeGenState extends CodeGenBase implements SessionState {
this.currentCodeGenerationId
)
- if (!action.tokenSource?.token.isCancellationRequested) {
+ if (!this.isCancellationRequested) {
action.messenger.sendAnswer({
message: i18n('AWS.amazonq.featureDev.pillText.generatingCode'),
type: 'answer-part',
@@ -302,7 +307,6 @@ export class CodeGenState extends CodeGenBase implements SessionState {
codeGenerationId,
telemetry: action.telemetry,
workspaceFolders: this.config.workspaceFolders,
- isCancellationRequested: action.tokenSource?.token.isCancellationRequested,
})
if (codeGeneration && !action.tokenSource?.token.isCancellationRequested) {
From 675c623b8378d6d45965b41281ec738734ff5db0 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Wed, 2 Oct 2024 15:58:52 -0400
Subject: [PATCH 15/87] fix(amazonqFeatureDev): don't share state to prepare
code since code cancellation should be in gen
---
packages/core/src/amazonqFeatureDev/session/sessionState.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index d983cba1cab..ea9041c9c86 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -331,7 +331,7 @@ export class CodeGenState extends CodeGenBase implements SessionState {
this.currentIteration + 1,
this.codeGenerationRemainingIterationCount,
this.codeGenerationTotalIterationCount,
- action.tokenSource,
+ this.tokenSource,
this.currentCodeGenerationId
)
return {
From 2c92f5ced1f27b3b68e5c75180cba8d7dd431634 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Thu, 3 Oct 2024 11:13:54 -0400
Subject: [PATCH 16/87] fix(amazonqFeatureDev): allow follow up options after
finally only
---
packages/core/package.nls.json | 3 ++-
.../src/amazonqFeatureDev/controllers/chat/controller.ts | 9 +++++++--
2 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json
index 467c7457dec..f16aa9e8f92 100644
--- a/packages/core/package.nls.json
+++ b/packages/core/package.nls.json
@@ -296,7 +296,8 @@
"AWS.amazonq.featureDev.pillText.generatingCode": "Generating code...",
"AWS.amazonq.featureDev.pillText.requestingChanges": "Requesting changes ...",
"AWS.amazonq.featureDev.pillText.insertCode": "Accept code",
- "AWS.amazonq.featureDev.pillText.stoppedCodeGeneration": "You've stopped the code generation.",
+ "AWS.amazonq.featureDev.pillText.stoppingCodeGeneration": "Stopping code generation ...",
+ "AWS.amazonq.featureDev.pillText.stoppedCodeGeneration": "The code generation has stopped.",
"AWS.amazonq.featureDev.pillText.sendFeedback": "Send feedback",
"AWS.amazonq.featureDev.pillText.selectFiles": "Select files for context",
"AWS.amazonq.featureDev.pillText.retry": "Retry",
diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
index fd83c2b6bdd..a44f253f059 100644
--- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
+++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
@@ -425,6 +425,7 @@ export class FeatureDevController {
if (session?.state?.tokenSource?.token.isCancellationRequested) {
this.disposeToken(session)
+ this.workOnNewTask(session)
} else {
this.messenger.sendAsyncEventProgress(tabID, false, undefined)
@@ -446,6 +447,11 @@ export class FeatureDevController {
}
}
private workOnNewTask(message: any) {
+ this.messenger.sendAnswer({
+ message: i18n('AWS.amazonq.featureDev.pillText.stoppedCodeGeneration'),
+ type: 'answer-part',
+ tabID: message.tabID,
+ })
this.messenger.sendAnswer({
type: 'system-prompt',
tabID: message.tabID,
@@ -713,11 +719,10 @@ export class FeatureDevController {
private async stopResponse(message: any) {
this.messenger.sendAnswer({
- message: i18n('AWS.amazonq.featureDev.pillText.stoppedCodeGeneration'),
+ message: i18n('AWS.amazonq.featureDev.pillText.stoppingCodeGeneration'),
type: 'answer-part',
tabID: message.tabID,
})
- this.workOnNewTask(message)
const session = await this.sessionStorage.getSession(message.tabID)
if (session.state?.tokenSource) {
session.state?.tokenSource?.cancel()
From 394c512c37ebb4b619f28186de0f5750a14d7f8d Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Thu, 3 Oct 2024 12:07:42 -0400
Subject: [PATCH 17/87] fix(amazonqFeatureDev): disable input placeholder until
its done
---
.../src/amazonqFeatureDev/controllers/chat/controller.ts | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
index a44f253f059..20ab35801e7 100644
--- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
+++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
@@ -723,6 +723,11 @@ export class FeatureDevController {
type: 'answer-part',
tabID: message.tabID,
})
+ this.messenger.sendChatInputEnabled(message.tabID, false)
+ this.messenger.sendUpdatePlaceholder(
+ message.tabID,
+ i18n('AWS.amazonq.featureDev.pillText.stoppingCodeGeneration')
+ )
const session = await this.sessionStorage.getSession(message.tabID)
if (session.state?.tokenSource) {
session.state?.tokenSource?.cancel()
From 0743620ac8118d17a2f2711f636e6a95c3fb687b Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Thu, 3 Oct 2024 12:18:01 -0400
Subject: [PATCH 18/87] fix(amazonqFeatureDev): disable input
---
.../core/src/amazonqFeatureDev/controllers/chat/controller.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
index 20ab35801e7..7b60971c482 100644
--- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
+++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
@@ -723,11 +723,12 @@ export class FeatureDevController {
type: 'answer-part',
tabID: message.tabID,
})
- this.messenger.sendChatInputEnabled(message.tabID, false)
this.messenger.sendUpdatePlaceholder(
message.tabID,
i18n('AWS.amazonq.featureDev.pillText.stoppingCodeGeneration')
)
+ this.messenger.sendChatInputEnabled(message.tabID, false)
+
const session = await this.sessionStorage.getSession(message.tabID)
if (session.state?.tokenSource) {
session.state?.tokenSource?.cancel()
From 30735e8d1f651c6150f4835321e0d5958d9b87b2 Mon Sep 17 00:00:00 2001
From: Kelvin Chu
Date: Tue, 8 Oct 2024 15:56:20 -0400
Subject: [PATCH 19/87] telemetry(amazonq): add metrics for stop code
generation
---
.../webview/ui/apps/featureDevChatConnector.ts | 12 ++++++++----
.../src/shared/telemetry/vscodeTelemetry.json | 15 +++++++++++++++
2 files changed, 23 insertions(+), 4 deletions(-)
diff --git a/packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts
index 6ee1828801a..63e68cc11b9 100644
--- a/packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts
+++ b/packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts
@@ -10,6 +10,7 @@ import { CodeReference } from './amazonqCommonsConnector'
import { FollowUpGenerator } from '../followUps/generator'
import { getActions } from '../diffTree/actions'
import { DiffTreeFileInfo } from '../diffTree/types'
+import { telemetry } from '../../../../shared/telemetry'
interface ChatPayload {
chatMessage: string
@@ -277,10 +278,13 @@ export class Connector {
}
onStopChatResponse = (tabID: string): void => {
- this.sendMessageToExtension({
- tabID: tabID,
- command: 'stop-response',
- tabType: 'featuredev',
+ telemetry.amazonq_stopCodeGeneration.run((span) => {
+ span.record({ tabID })
+ this.sendMessageToExtension({
+ tabID: tabID,
+ command: 'stop-response',
+ tabType: 'featuredev',
+ })
})
}
diff --git a/packages/core/src/shared/telemetry/vscodeTelemetry.json b/packages/core/src/shared/telemetry/vscodeTelemetry.json
index 1a6f82b977a..525e094b4c4 100644
--- a/packages/core/src/shared/telemetry/vscodeTelemetry.json
+++ b/packages/core/src/shared/telemetry/vscodeTelemetry.json
@@ -358,6 +358,11 @@
"name": "amazonqMessageDisplayedMs",
"type": "int",
"description": "Duration between the partner teams code receiving the message and when the message was finally displayed in ms"
+ },
+ {
+ "name": "tabID",
+ "type": "string",
+ "description": "The unique identifier of a tab"
}
],
"metrics": [
@@ -1260,6 +1265,16 @@
"required": false
}
]
+ },
+ {
+ "name": "amazonq_stopCodeGeneration",
+ "description": "User stopped the code generation",
+ "metadata": [
+ {
+ "type": "tabID",
+ "required": true
+ }
+ ]
}
]
}
From 047125bb809a0209f4c4ea062c47d62f74de2643 Mon Sep 17 00:00:00 2001
From: Kelvin Chu
Date: Thu, 10 Oct 2024 15:39:32 -0400
Subject: [PATCH 20/87] telemetry(amazonq): add metrics for stop code
generation
---
.../ui/apps/featureDevChatConnector.ts | 12 +++----
.../controllers/chat/controller.ts | 31 ++++++++++---------
.../controllers/chat/controller.test.ts | 14 +++++++++
3 files changed, 35 insertions(+), 22 deletions(-)
diff --git a/packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts
index 63e68cc11b9..6ee1828801a 100644
--- a/packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts
+++ b/packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts
@@ -10,7 +10,6 @@ import { CodeReference } from './amazonqCommonsConnector'
import { FollowUpGenerator } from '../followUps/generator'
import { getActions } from '../diffTree/actions'
import { DiffTreeFileInfo } from '../diffTree/types'
-import { telemetry } from '../../../../shared/telemetry'
interface ChatPayload {
chatMessage: string
@@ -278,13 +277,10 @@ export class Connector {
}
onStopChatResponse = (tabID: string): void => {
- telemetry.amazonq_stopCodeGeneration.run((span) => {
- span.record({ tabID })
- this.sendMessageToExtension({
- tabID: tabID,
- command: 'stop-response',
- tabType: 'featuredev',
- })
+ this.sendMessageToExtension({
+ tabID: tabID,
+ command: 'stop-response',
+ tabType: 'featuredev',
})
}
diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
index 00345002dd6..a09341d9614 100644
--- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
+++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
@@ -749,21 +749,24 @@ export class FeatureDevController {
}
private async stopResponse(message: any) {
- this.messenger.sendAnswer({
- message: i18n('AWS.amazonq.featureDev.pillText.stoppingCodeGeneration'),
- type: 'answer-part',
- tabID: message.tabID,
- })
- this.messenger.sendUpdatePlaceholder(
- message.tabID,
- i18n('AWS.amazonq.featureDev.pillText.stoppingCodeGeneration')
- )
- this.messenger.sendChatInputEnabled(message.tabID, false)
+ await telemetry.amazonq_stopCodeGeneration.run(async (span) => {
+ span.record({ tabID: message.tabID })
+ this.messenger.sendAnswer({
+ message: i18n('AWS.amazonq.featureDev.pillText.stoppingCodeGeneration'),
+ type: 'answer-part',
+ tabID: message.tabID,
+ })
+ this.messenger.sendUpdatePlaceholder(
+ message.tabID,
+ i18n('AWS.amazonq.featureDev.pillText.stoppingCodeGeneration')
+ )
+ this.messenger.sendChatInputEnabled(message.tabID, false)
- const session = await this.sessionStorage.getSession(message.tabID)
- if (session.state?.tokenSource) {
- session.state?.tokenSource?.cancel()
- }
+ const session = await this.sessionStorage.getSession(message.tabID)
+ if (session.state?.tokenSource) {
+ session.state?.tokenSource?.cancel()
+ }
+ })
}
private async tabOpened(message: any) {
diff --git a/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts b/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts
index 1f72aa6f270..6e57ed3c34f 100644
--- a/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts
+++ b/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts
@@ -404,4 +404,18 @@ describe('Controller', () => {
})
})
})
+
+ describe('stopResponse', () => {
+ it('should emit amazonq_stopCodeGeneration telemetry', async () => {
+ const getSessionStub = sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session)
+ controllerSetup.emitters.stopResponse.fire({ tabID, conversationID })
+ await waitUntil(() => {
+ return Promise.resolve(getSessionStub.callCount > 0)
+ }, {})
+ assertTelemetry('amazonq_stopCodeGeneration', {
+ tabID: tabID,
+ result: 'Succeeded',
+ })
+ })
+ })
})
From 20e549355e3c676ae39a75628a6d82b33237b49a Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Tue, 15 Oct 2024 15:23:27 -0400
Subject: [PATCH 21/87] fix(dev): merge conflicts and missing commas
---
.../core/src/amazonqFeatureDev/session/sessionState.ts | 7 ++++---
packages/core/src/amazonqFeatureDev/types.ts | 1 -
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index f61530b08f8..fd0353c0d65 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -364,7 +364,7 @@ export class CodeGenState extends CodeGenBase implements SessionState {
this.codeGenerationRemainingIterationCount,
this.codeGenerationTotalIterationCount,
this.tokenSource,
- this.currentCodeGenerationId
+ this.currentCodeGenerationId,
action.uploadHistory,
codeGenerationId
)
@@ -486,11 +486,12 @@ export class PrepareCodeGenState implements SessionState {
public references: CodeReference[],
public tabID: string,
private currentIteration: number,
- public uploadHistory: UploadHistory = {},
+
public codeGenerationRemainingIterationCount?: number,
public codeGenerationTotalIterationCount?: number,
public superTokenSource?: vscode.CancellationTokenSource,
- public currentCodeGenerationId?: string
+ public currentCodeGenerationId?: string,
+ public uploadHistory: UploadHistory = {},
public codeGenerationId?: string
) {
this.tokenSource = superTokenSource || new vscode.CancellationTokenSource()
diff --git a/packages/core/src/amazonqFeatureDev/types.ts b/packages/core/src/amazonqFeatureDev/types.ts
index 5df8b2c0436..0ed49fcc267 100644
--- a/packages/core/src/amazonqFeatureDev/types.ts
+++ b/packages/core/src/amazonqFeatureDev/types.ts
@@ -61,7 +61,6 @@ export interface SessionState {
readonly references?: CodeReference[]
readonly phase?: SessionStatePhase
readonly uploadId: string
- readonly tokenSource: CancellationTokenSource
currentCodeGenerationId?: string
tokenSource?: CancellationTokenSource
readonly codeGenerationId?: string
From 9597f2506291c395f4153dd552a477971cb63c7f Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Wed, 16 Oct 2024 10:03:56 -0400
Subject: [PATCH 22/87] fix(dev): update strings
---
packages/core/package.nls.json | 5 +-
.../src/amazonq/webview/ui/texts/constants.ts | 2 +-
.../controllers/chat/controller.ts | 70 ++++++++++++-------
.../amazonqFeatureDev/session/sessionState.ts | 4 +-
packages/core/src/amazonqFeatureDev/types.ts | 1 +
5 files changed, 52 insertions(+), 30 deletions(-)
diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json
index c7e6d2d368b..c2d3d644214 100644
--- a/packages/core/package.nls.json
+++ b/packages/core/package.nls.json
@@ -298,8 +298,7 @@
"AWS.amazonq.featureDev.pillText.generatingCode": "Generating code...",
"AWS.amazonq.featureDev.pillText.requestingChanges": "Requesting changes ...",
"AWS.amazonq.featureDev.pillText.insertCode": "Accept code",
- "AWS.amazonq.featureDev.pillText.stoppingCodeGeneration": "Stopping code generation ...",
- "AWS.amazonq.featureDev.pillText.stoppedCodeGeneration": "The code generation has stopped.",
+ "AWS.amazonq.featureDev.pillText.stoppingCodeGeneration": "Stopping code generation...",
"AWS.amazonq.featureDev.pillText.sendFeedback": "Send feedback",
"AWS.amazonq.featureDev.pillText.selectFiles": "Select files for context",
"AWS.amazonq.featureDev.pillText.retry": "Retry",
@@ -316,7 +315,7 @@
"AWS.amazonq.featureDev.answer.sessionClosed": "Okay, I've ended this chat session. You can open a new tab to chat or start another workflow.",
"AWS.amazonq.featureDev.answer.newTaskChanges": "What new task would you like to work on?",
"AWS.amazonq.featureDev.placeholder.chatInputDisabled": "Chat input is disabled",
- "AWS.amazonq.featureDev.placeholder.additionalImprovements": "Choose an option to proceed",
+ "AWS.amazonq.featureDev.placeholder.additionalImprovements": "Describe your task or issue in detail",
"AWS.amazonq.featureDev.placeholder.feedback": "Provide feedback or comments",
"AWS.amazonq.featureDev.placeholder.describe": "Describe your task or issue in detail",
"AWS.amazonq.featureDev.placeholder.sessionClosed": "Open a new chat tab to continue"
diff --git a/packages/core/src/amazonq/webview/ui/texts/constants.ts b/packages/core/src/amazonq/webview/ui/texts/constants.ts
index d0fb74469ee..5ac1f34fda9 100644
--- a/packages/core/src/amazonq/webview/ui/texts/constants.ts
+++ b/packages/core/src/amazonq/webview/ui/texts/constants.ts
@@ -19,7 +19,7 @@ export const uiComponentsTexts = {
save: 'Save',
cancel: 'Cancel',
submit: 'Submit',
- stopGenerating: 'Stop generating',
+ stopGenerating: 'Stop',
copyToClipboard: 'Copied to clipboard',
noMoreTabsTooltip: 'You can only open ten conversation tabs at a time.',
codeSuggestionWithReferenceTitle: 'Some suggestions contain code with references.',
diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
index a09341d9614..3dce78c8233 100644
--- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
+++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
@@ -450,8 +450,13 @@ export class FeatureDevController {
// Finish processing the event
if (session?.state?.tokenSource?.token.isCancellationRequested) {
+ this.workOnNewTask(
+ session,
+ session.state.codeGenerationRemainingIterationCount || session.state?.currentIteration,
+ session.state.codeGenerationTotalIterationCount,
+ session?.state?.tokenSource?.token.isCancellationRequested
+ )
this.disposeToken(session)
- this.workOnNewTask(session)
} else {
this.messenger.sendAsyncEventProgress(tabID, false, undefined)
@@ -472,28 +477,41 @@ export class FeatureDevController {
}
}
}
- private workOnNewTask(message: any) {
- this.messenger.sendAnswer({
- message: i18n('AWS.amazonq.featureDev.pillText.stoppedCodeGeneration'),
- type: 'answer-part',
- tabID: message.tabID,
- })
- this.messenger.sendAnswer({
- type: 'system-prompt',
- tabID: message.tabID,
- followUps: [
- {
- pillText: i18n('AWS.amazonq.featureDev.pillText.newTask'),
- type: FollowUpTypes.NewTask,
- status: 'info',
- },
- {
- pillText: i18n('AWS.amazonq.featureDev.pillText.closeSession'),
- type: FollowUpTypes.CloseSession,
- status: 'info',
- },
- ],
- })
+ private workOnNewTask(
+ message: any,
+ remainingIterations?: number,
+ totalIterations?: number,
+ isStoppedGeneration: boolean = false
+ ) {
+ if (isStoppedGeneration) {
+ this.messenger.sendAnswer({
+ message:
+ remainingIterations === 0
+ ? "I stopped generating your code. You don't have more iterations left, however, you can start a new session."
+ : `I stopped generating your code. If you want to continue working on this task, provide another description. ${!totalIterations ? `You have started ${remainingIterations} code generations.` : `You have ${remainingIterations} out of ${totalIterations} code generations left.`}`,
+ type: 'answer-part',
+ tabID: message.tabID,
+ })
+ }
+
+ if ((remainingIterations === 0 && isStoppedGeneration) || !isStoppedGeneration) {
+ this.messenger.sendAnswer({
+ type: 'system-prompt',
+ tabID: message.tabID,
+ followUps: [
+ {
+ pillText: i18n('AWS.amazonq.featureDev.pillText.newTask'),
+ type: FollowUpTypes.NewTask,
+ status: 'info',
+ },
+ {
+ pillText: i18n('AWS.amazonq.featureDev.pillText.closeSession'),
+ type: FollowUpTypes.CloseSession,
+ status: 'info',
+ },
+ ],
+ })
+ }
// Ensure that chat input is enabled so that they can provide additional iterations if they choose
this.messenger.sendChatInputEnabled(message.tabID, true)
@@ -528,7 +546,11 @@ export class FeatureDevController {
canBeVoted: true,
})
- this.workOnNewTask(message)
+ this.workOnNewTask(
+ message,
+ session.state.codeGenerationRemainingIterationCount,
+ session.state.codeGenerationTotalIterationCount
+ )
} catch (err: any) {
this.messenger.sendErrorMessage(
createUserFacingErrorMessage(`Failed to insert code changes: ${err.message}`),
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index fd0353c0d65..03b6e78e178 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -278,7 +278,7 @@ export class CodeGenState extends CodeGenBase implements SessionState {
public deletedFiles: DeletedFileInfo[],
public references: CodeReference[],
tabID: string,
- private currentIteration: number,
+ public currentIteration: number,
public uploadHistory: UploadHistory,
public codeGenerationRemainingIterationCount?: number,
public codeGenerationTotalIterationCount?: number
@@ -485,7 +485,7 @@ export class PrepareCodeGenState implements SessionState {
public deletedFiles: DeletedFileInfo[],
public references: CodeReference[],
public tabID: string,
- private currentIteration: number,
+ public currentIteration: number,
public codeGenerationRemainingIterationCount?: number,
public codeGenerationTotalIterationCount?: number,
diff --git a/packages/core/src/amazonqFeatureDev/types.ts b/packages/core/src/amazonqFeatureDev/types.ts
index 0ed49fcc267..c4267d91e29 100644
--- a/packages/core/src/amazonqFeatureDev/types.ts
+++ b/packages/core/src/amazonqFeatureDev/types.ts
@@ -61,6 +61,7 @@ export interface SessionState {
readonly references?: CodeReference[]
readonly phase?: SessionStatePhase
readonly uploadId: string
+ readonly currentIteration?: number
currentCodeGenerationId?: string
tokenSource?: CancellationTokenSource
readonly codeGenerationId?: string
From e9ef77092c60d9cc5a3dee3cfda786dd6d5d0d3b Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Wed, 16 Oct 2024 10:05:50 -0400
Subject: [PATCH 23/87] chore(dev): include changelog
---
.../Feature-3f18ffa1-f90f-4ceb-8af9-a913a56cd5ce.json | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 packages/amazonq/.changes/next-release/Feature-3f18ffa1-f90f-4ceb-8af9-a913a56cd5ce.json
diff --git a/packages/amazonq/.changes/next-release/Feature-3f18ffa1-f90f-4ceb-8af9-a913a56cd5ce.json b/packages/amazonq/.changes/next-release/Feature-3f18ffa1-f90f-4ceb-8af9-a913a56cd5ce.json
new file mode 100644
index 00000000000..8a226747b15
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Feature-3f18ffa1-f90f-4ceb-8af9-a913a56cd5ce.json
@@ -0,0 +1,4 @@
+{
+ "type": "Feature",
+ "description": "Amazon Q /dev: Add stop generation action"
+}
From 5e194ae1885d04d3b9d8d65967a35d85445ae354 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Wed, 16 Oct 2024 11:45:55 -0400
Subject: [PATCH 24/87] fix(dev): update tests
---
.../src/test/amazonqFeatureDev/session/sessionState.test.ts | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts b/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts
index c62266364f8..8a67bfcbb46 100644
--- a/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts
+++ b/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts
@@ -38,10 +38,12 @@ const mockSessionStateConfig = ({
conversationId,
uploadId,
workspaceFolder,
+ currentCodeGenerationId,
}: {
conversationId: string
uploadId: string
workspaceFolder: vscode.WorkspaceFolder
+ currentCodeGenerationId?: string
}): SessionStateConfig => ({
workspaceRoots: ['fake-source'],
workspaceFolders: [workspaceFolder],
@@ -54,12 +56,14 @@ const mockSessionStateConfig = ({
exportResultArchive: () => mockExportResultArchive(),
} as unknown as FeatureDevClient,
uploadId,
+ currentCodeGenerationId,
})
describe('sessionState', () => {
const conversationId = 'conversation-id'
const uploadId = 'upload-id'
const tabId = 'tab-id'
+ const currentCodeGenerationId = ''
let testConfig: SessionStateConfig
beforeEach(async () => {
@@ -67,6 +71,7 @@ describe('sessionState', () => {
conversationId,
uploadId,
workspaceFolder: await createTestWorkspaceFolder('fake-root'),
+ currentCodeGenerationId,
})
})
From aaa0a5d3187d3c472f2df993d55f44b81f46e858 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Wed, 16 Oct 2024 12:19:19 -0400
Subject: [PATCH 25/87] fix(dev): fill args in constructor
---
.../session/sessionState.test.ts | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts b/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts
index 8a67bfcbb46..be61e082302 100644
--- a/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts
+++ b/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts
@@ -112,13 +112,27 @@ describe('sessionState', () => {
codeGenerationRemainingIterationCount: 2,
codeGenerationTotalIterationCount: 3,
})
+
+ const currentCodeGenerationId = crypto.randomUUID()
mockExportResultArchive = sinon.stub().resolves({ newFileContents: [], deletedFiles: [], references: [] })
const testAction = mockSessionStateAction()
const state = new CodeGenState(testConfig, [], [], [], tabId, 0, {}, 2, 3)
const result = await state.interact(testAction)
- const nextState = new PrepareCodeGenState(testConfig, [], [], [], tabId, 1, 2, 3)
+ const nextState = new PrepareCodeGenState(
+ testConfig,
+ [],
+ [],
+ [],
+ tabId,
+ 1,
+ 2,
+ 3,
+ undefined,
+ currentCodeGenerationId,
+ {}
+ )
assert.deepStrictEqual(result, {
nextState,
From 8f7fa0744139c30850129b7d4094da07ed02f8b9 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Wed, 16 Oct 2024 12:50:47 -0400
Subject: [PATCH 26/87] fix(dev): update failing test to assert items that
should be equal
---
.../session/sessionState.test.ts | 27 ++++++++-----------
1 file changed, 11 insertions(+), 16 deletions(-)
diff --git a/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts b/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts
index be61e082302..24e7ca15d3f 100644
--- a/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts
+++ b/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts
@@ -113,29 +113,24 @@ describe('sessionState', () => {
codeGenerationTotalIterationCount: 3,
})
- const currentCodeGenerationId = crypto.randomUUID()
mockExportResultArchive = sinon.stub().resolves({ newFileContents: [], deletedFiles: [], references: [] })
const testAction = mockSessionStateAction()
const state = new CodeGenState(testConfig, [], [], [], tabId, 0, {}, 2, 3)
const result = await state.interact(testAction)
- const nextState = new PrepareCodeGenState(
- testConfig,
- [],
- [],
- [],
- tabId,
- 1,
- 2,
- 3,
- undefined,
- currentCodeGenerationId,
- {}
- )
+ const nextState = new PrepareCodeGenState(testConfig, [], [], [], tabId, 1, 2, 3, undefined)
- assert.deepStrictEqual(result, {
- nextState,
+ assert.deepStrictEqual(result.nextState?.deletedFiles, {
+ nextState: nextState.deletedFiles,
+ interaction: {},
+ })
+ assert.deepStrictEqual(result.nextState?.filePaths, {
+ nextState: nextState.filePaths,
+ interaction: {},
+ })
+ assert.deepStrictEqual(result.nextState?.references, {
+ nextState: nextState.references,
interaction: {},
})
})
From e63536b5c4f0b7c2fef9b7d6926ca41b3644a9e7 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Wed, 16 Oct 2024 13:02:30 -0400
Subject: [PATCH 27/87] fix(dev): validate files and paths on failing test
---
.../session/sessionState.test.ts | 15 +++------------
1 file changed, 3 insertions(+), 12 deletions(-)
diff --git a/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts b/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts
index 24e7ca15d3f..ec9cf8ea396 100644
--- a/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts
+++ b/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts
@@ -121,18 +121,9 @@ describe('sessionState', () => {
const nextState = new PrepareCodeGenState(testConfig, [], [], [], tabId, 1, 2, 3, undefined)
- assert.deepStrictEqual(result.nextState?.deletedFiles, {
- nextState: nextState.deletedFiles,
- interaction: {},
- })
- assert.deepStrictEqual(result.nextState?.filePaths, {
- nextState: nextState.filePaths,
- interaction: {},
- })
- assert.deepStrictEqual(result.nextState?.references, {
- nextState: nextState.references,
- interaction: {},
- })
+ assert.deepStrictEqual(result.nextState?.deletedFiles, nextState.deletedFiles)
+ assert.deepStrictEqual(result.nextState?.filePaths, result.nextState?.filePaths)
+ assert.deepStrictEqual(result.nextState?.references, result.nextState?.references)
})
it('fails when codeGenerationStatus failed ', async () => {
From 326b4a9357b8f487fbfa6a4e04c2fe83b58683e8 Mon Sep 17 00:00:00 2001
From: Vikash Agrawal
Date: Thu, 10 Oct 2024 11:47:46 -0700
Subject: [PATCH 28/87] refactor(amazonq): move getIndentedCode, cleanup
telemetry #5765
- move getIndentedCode to textDocumentUtilities
- use cwsprChatInteractionType from aws-toolkit-common
---
.../commons/controllers/contentController.ts | 3 ++-
.../core/src/amazonq/util/functionUtils.ts | 2 +-
.../src/shared/telemetry/vscodeTelemetry.json | 18 ----------------
.../shared/utilities/textDocumentUtilities.ts | 21 ++++++++++++++++++-
.../src/shared/utilities/textUtilities.ts | 19 -----------------
5 files changed, 23 insertions(+), 40 deletions(-)
diff --git a/packages/core/src/amazonq/commons/controllers/contentController.ts b/packages/core/src/amazonq/commons/controllers/contentController.ts
index edcb84f9fb0..821e2988f96 100644
--- a/packages/core/src/amazonq/commons/controllers/contentController.ts
+++ b/packages/core/src/amazonq/commons/controllers/contentController.ts
@@ -12,9 +12,10 @@ import { disposeOnEditorClose } from '../../../shared/utilities/editorUtilities'
import {
applyChanges,
createTempFileForDiff,
+ getIndentedCode,
getSelectionFromRange,
} from '../../../shared/utilities/textDocumentUtilities'
-import { extractFileAndCodeSelectionFromMessage, fs, getErrorMsg, getIndentedCode, ToolkitError } from '../../../shared'
+import { extractFileAndCodeSelectionFromMessage, fs, getErrorMsg, ToolkitError } from '../../../shared'
class ContentProvider implements vscode.TextDocumentContentProvider {
constructor(private uri: vscode.Uri) {}
diff --git a/packages/core/src/amazonq/util/functionUtils.ts b/packages/core/src/amazonq/util/functionUtils.ts
index c658b59aeef..b5d6a9bb9dc 100644
--- a/packages/core/src/amazonq/util/functionUtils.ts
+++ b/packages/core/src/amazonq/util/functionUtils.ts
@@ -4,7 +4,7 @@
*/
/**
- * Converts an array of key-value pairs into a Map object.
+ * Tries to create map and returns empty map if failed.
*
* @param {[unknown, unknown][]} arr - An array of tuples, where each tuple represents a key-value pair.
* @returns {Map} A new Map object created from the input array.
diff --git a/packages/core/src/shared/telemetry/vscodeTelemetry.json b/packages/core/src/shared/telemetry/vscodeTelemetry.json
index 525e094b4c4..36a4b110583 100644
--- a/packages/core/src/shared/telemetry/vscodeTelemetry.json
+++ b/packages/core/src/shared/telemetry/vscodeTelemetry.json
@@ -199,24 +199,6 @@
"type": "int",
"description": "Number of characters in request"
},
- {
- "name": "cwsprChatInteractionType",
- "allowedValues": [
- "acceptDiff",
- "insertAtCursor",
- "copySnippet",
- "copy",
- "clickLink",
- "clickFollowUp",
- "hoverReference",
- "upvote",
- "downvote",
- "clickBodyLink",
- "viewDiff"
- ],
- "type": "string",
- "description": "Indicates the specific interaction type with a message in a conversation"
- },
{
"name": "cwsprChatInteractionTarget",
"type": "string",
diff --git a/packages/core/src/shared/utilities/textDocumentUtilities.ts b/packages/core/src/shared/utilities/textDocumentUtilities.ts
index 4d5805ef639..71114bd2389 100644
--- a/packages/core/src/shared/utilities/textDocumentUtilities.ts
+++ b/packages/core/src/shared/utilities/textDocumentUtilities.ts
@@ -7,7 +7,7 @@ import * as _path from 'path'
import * as vscode from 'vscode'
import { getTabSizeSetting } from './editorUtilities'
import { tempDirPath } from '../filesystemUtilities'
-import { fs, getIndentedCode, ToolkitError } from '../index'
+import { fs, indent, ToolkitError } from '../index'
import { getLogger } from '../logger'
/**
@@ -165,3 +165,22 @@ export async function createTempFileForDiff(
await applyChanges(doc, range, code)
return tempFileUri
}
+
+/**
+ * Indents the given code based on the current document's indentation at the selection start.
+ *
+ * @param message The message object containing the code.
+ * @param doc The VSCode document where the code is applied.
+ * @param selection The selection range in the document.
+ * @returns The processed code to be applied to the document.
+ */
+export function getIndentedCode(message: any, doc: vscode.TextDocument, selection: vscode.Selection) {
+ const indentRange = new vscode.Range(new vscode.Position(selection.start.line, 0), selection.active)
+ let indentation = doc.getText(indentRange)
+
+ if (indentation.trim().length !== 0) {
+ indentation = ' '.repeat(indentation.length - indentation.trimStart().length)
+ }
+
+ return indent(message.code, indentation.length)
+}
diff --git a/packages/core/src/shared/utilities/textUtilities.ts b/packages/core/src/shared/utilities/textUtilities.ts
index d0af2e6de52..f337bb31276 100644
--- a/packages/core/src/shared/utilities/textUtilities.ts
+++ b/packages/core/src/shared/utilities/textUtilities.ts
@@ -417,22 +417,3 @@ export function extractFileAndCodeSelectionFromMessage(message: any) {
const selection = message?.context?.focusAreaContext?.selectionInsideExtendedCodeBlock as vscode.Selection
return { filePath, selection }
}
-
-/**
- * Indents the given code based on the current document's indentation at the selection start.
- *
- * @param {any} message - The message object containing the code.
- * @param {vscode.TextDocument} doc - The VSCode document where the code is applied.
- * @param {vscode.Selection} selection - The selection range in the document.
- * @returns {string} - The processed code to be applied to the document.
- */
-export function getIndentedCode(message: any, doc: vscode.TextDocument, selection: vscode.Selection) {
- const indentRange = new vscode.Range(new vscode.Position(selection.start.line, 0), selection.active)
- let indentation = doc.getText(indentRange)
-
- if (indentation.trim().length !== 0) {
- indentation = ' '.repeat(indentation.length - indentation.trimStart().length)
- }
-
- return indent(message.code, indentation.length)
-}
From 937fb13cc52a5639e6c66c08f80bb73244c7425b Mon Sep 17 00:00:00 2001
From: andrewyuq <89420755+andrewyuq@users.noreply.github.com>
Date: Thu, 10 Oct 2024 12:51:58 -0700
Subject: [PATCH 29/87] telemetry(amazonq): Add new fields in
UserTriggerDecision and UserModification events (#5766)
1. bring back UserModification SendTelemetryEvent and track
acceptedCharacterCount and unmodifiedAcceptedCharacterCount.
2. combine some latency tracking to the session object
JB PR reference: https://github.com/aws/aws-toolkit-jetbrains/pull/4955
## Problem
## Solution
---
License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---
.../codewhispererCodeCoverageTracker.test.ts | 23 +++++----
.../codewhisperer/client/user-service-2.json | 18 +++++--
.../service/inlineCompletionService.ts | 1 -
.../service/recommendationHandler.ts | 2 +-
.../codewhispererCodeCoverageTracker.ts | 12 +----
.../tracker/codewhispererTracker.ts | 48 +++++++++++++++----
.../util/codeWhispererSession.ts | 18 ++++++-
.../core/src/codewhisperer/util/commonUtil.ts | 10 ++++
.../src/codewhisperer/util/telemetryHelper.ts | 44 +++++++----------
9 files changed, 116 insertions(+), 60 deletions(-)
diff --git a/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererCodeCoverageTracker.test.ts b/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererCodeCoverageTracker.test.ts
index 7fe3b3b7840..ee001b3328d 100644
--- a/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererCodeCoverageTracker.test.ts
+++ b/packages/amazonq/test/unit/codewhisperer/tracker/codewhispererCodeCoverageTracker.test.ts
@@ -6,7 +6,13 @@
import assert from 'assert'
import * as sinon from 'sinon'
import * as vscode from 'vscode'
-import { CodeWhispererCodeCoverageTracker, vsCodeState, TelemetryHelper, AuthUtil } from 'aws-core-vscode/codewhisperer'
+import {
+ CodeWhispererCodeCoverageTracker,
+ vsCodeState,
+ TelemetryHelper,
+ AuthUtil,
+ getUnmodifiedAcceptedTokens,
+} from 'aws-core-vscode/codewhisperer'
import { createMockDocument, createMockTextEditor, resetCodeWhispererGlobalVariables } from 'aws-core-vscode/test'
import { globals } from 'aws-core-vscode/shared'
import { assertTelemetryCurried } from 'aws-core-vscode/test'
@@ -150,14 +156,13 @@ describe('codewhispererCodecoverageTracker', function () {
})
it('Should return correct unmodified accepted tokens count', function () {
- const tracker = CodeWhispererCodeCoverageTracker.getTracker(language)
- assert.strictEqual(tracker?.getUnmodifiedAcceptedTokens('foo', 'fou'), 2)
- assert.strictEqual(tracker?.getUnmodifiedAcceptedTokens('foo', 'f11111oo'), 3)
- assert.strictEqual(tracker?.getUnmodifiedAcceptedTokens('foo', 'fo'), 2)
- assert.strictEqual(tracker?.getUnmodifiedAcceptedTokens('helloworld', 'HelloWorld'), 8)
- assert.strictEqual(tracker?.getUnmodifiedAcceptedTokens('helloworld', 'World'), 4)
- assert.strictEqual(tracker?.getUnmodifiedAcceptedTokens('CodeWhisperer', 'CODE'), 1)
- assert.strictEqual(tracker?.getUnmodifiedAcceptedTokens('CodeWhisperer', 'CodeWhispererGood'), 13)
+ assert.strictEqual(getUnmodifiedAcceptedTokens('foo', 'fou'), 2)
+ assert.strictEqual(getUnmodifiedAcceptedTokens('foo', 'f11111oo'), 3)
+ assert.strictEqual(getUnmodifiedAcceptedTokens('foo', 'fo'), 2)
+ assert.strictEqual(getUnmodifiedAcceptedTokens('helloworld', 'HelloWorld'), 8)
+ assert.strictEqual(getUnmodifiedAcceptedTokens('helloworld', 'World'), 4)
+ assert.strictEqual(getUnmodifiedAcceptedTokens('CodeWhisperer', 'CODE'), 1)
+ assert.strictEqual(getUnmodifiedAcceptedTokens('CodeWhisperer', 'CodeWhispererGood'), 13)
})
})
diff --git a/packages/core/src/codewhisperer/client/user-service-2.json b/packages/core/src/codewhisperer/client/user-service-2.json
index bda8d16922d..3a94931dddb 100644
--- a/packages/core/src/codewhisperer/client/user-service-2.json
+++ b/packages/core/src/codewhisperer/client/user-service-2.json
@@ -2197,14 +2197,24 @@
},
"UserModificationEvent": {
"type": "structure",
- "required": ["sessionId", "requestId", "programmingLanguage", "modificationPercentage", "timestamp"],
+ "required": [
+ "sessionId",
+ "requestId",
+ "programmingLanguage",
+ "modificationPercentage",
+ "timestamp",
+ "acceptedCharacterCount",
+ "unmodifiedAcceptedCharacterCount"
+ ],
"members": {
"sessionId": { "shape": "UUID" },
"requestId": { "shape": "UUID" },
"programmingLanguage": { "shape": "ProgrammingLanguage" },
"modificationPercentage": { "shape": "Double" },
"customizationArn": { "shape": "CustomizationArn" },
- "timestamp": { "shape": "Timestamp" }
+ "timestamp": { "shape": "Timestamp" },
+ "acceptedCharacterCount": { "shape": "PrimitiveInteger" },
+ "unmodifiedAcceptedCharacterCount": { "shape": "PrimitiveInteger" }
}
},
"UserTriggerDecisionEvent": {
@@ -2230,7 +2240,9 @@
"triggerToResponseLatencyMilliseconds": { "shape": "Double" },
"suggestionReferenceCount": { "shape": "PrimitiveInteger" },
"generatedLine": { "shape": "PrimitiveInteger" },
- "numberOfRecommendations": { "shape": "PrimitiveInteger" }
+ "numberOfRecommendations": { "shape": "PrimitiveInteger" },
+ "perceivedLatencyMilliseconds": { "shape": "Double" },
+ "acceptedCharacterCount": { "shape": "PrimitiveInteger" }
}
},
"ValidationException": {
diff --git a/packages/core/src/codewhisperer/service/inlineCompletionService.ts b/packages/core/src/codewhisperer/service/inlineCompletionService.ts
index 69bd9bdb887..715fd93ad2d 100644
--- a/packages/core/src/codewhisperer/service/inlineCompletionService.ts
+++ b/packages/core/src/codewhisperer/service/inlineCompletionService.ts
@@ -112,7 +112,6 @@ export class InlineCompletionService {
await this.setState('loading')
- TelemetryHelper.instance.setInvocationStartTime(performance.now())
RecommendationHandler.instance.checkAndResetCancellationTokens()
RecommendationHandler.instance.documentUri = editor.document.uri
let response: GetRecommendationsResponse = {
diff --git a/packages/core/src/codewhisperer/service/recommendationHandler.ts b/packages/core/src/codewhisperer/service/recommendationHandler.ts
index 60e877064bb..1fd46541c11 100644
--- a/packages/core/src/codewhisperer/service/recommendationHandler.ts
+++ b/packages/core/src/codewhisperer/service/recommendationHandler.ts
@@ -256,7 +256,7 @@ export class RecommendationHandler {
sessionId = resp?.$response?.httpResponse?.headers['x-amzn-sessionid']
TelemetryHelper.instance.setFirstResponseRequestId(requestId)
if (page === 0) {
- TelemetryHelper.instance.setTimeToFirstRecommendation(performance.now())
+ session.setTimeToFirstRecommendation(performance.now())
}
if (nextToken === '') {
TelemetryHelper.instance.setAllPaginationEndTime()
diff --git a/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts b/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts
index 2ec761ceb8c..925609ce185 100644
--- a/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts
+++ b/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts
@@ -8,7 +8,6 @@ import { getLogger } from '../../shared/logger/logger'
import * as CodeWhispererConstants from '../models/constants'
import globals from '../../shared/extensionGlobals'
import { vsCodeState } from '../models/model'
-import { distance } from 'fastest-levenshtein'
import { CodewhispererLanguage, telemetry } from '../../shared/telemetry/telemetry'
import { runtimeLanguageContext } from '../util/runtimeLanguageContext'
import { TelemetryHelper } from '../util/telemetryHelper'
@@ -16,6 +15,7 @@ import { AuthUtil } from '../util/authUtil'
import { getSelectedCustomization } from '../util/customizationUtil'
import { codeWhispererClient as client } from '../client/codewhisperer'
import { isAwsError } from '../../shared/errors'
+import { getUnmodifiedAcceptedTokens } from '../util/commonUtil'
interface CodeWhispererToken {
range: vscode.Range
@@ -86,18 +86,10 @@ export class CodeWhispererCodeCoverageTracker {
for (let i = 0; i < this._acceptedTokens[filename].length; i++) {
const oldText = this._acceptedTokens[filename][i].text
const newText = editor.document.getText(this._acceptedTokens[filename][i].range)
- this._acceptedTokens[filename][i].accepted = this.getUnmodifiedAcceptedTokens(oldText, newText)
+ this._acceptedTokens[filename][i].accepted = getUnmodifiedAcceptedTokens(oldText, newText)
}
}
}
- // With edit distance, complicate usermodification can be considered as simple edit(add, delete, replace),
- // and thus the unmodified part of recommendation length can be deducted/approximated
- // ex. (modified > original): originalRecom: foo -> modifiedRecom: fobarbarbaro, distance = 9, delta = 12 - 9 = 3
- // ex. (modified == original): originalRecom: helloworld -> modifiedRecom: HelloWorld, distance = 2, delta = 10 - 2 = 8
- // ex. (modified < original): originalRecom: CodeWhisperer -> modifiedRecom: CODE, distance = 12, delta = 13 - 12 = 1
- public getUnmodifiedAcceptedTokens(origin: string, after: string) {
- return Math.max(origin.length, after.length) - distance(origin, after)
- }
public emitCodeWhispererCodeContribution() {
let totalTokens = 0
diff --git a/packages/core/src/codewhisperer/tracker/codewhispererTracker.ts b/packages/core/src/codewhisperer/tracker/codewhispererTracker.ts
index 3322fa29990..05a6d83f3f0 100644
--- a/packages/core/src/codewhisperer/tracker/codewhispererTracker.ts
+++ b/packages/core/src/codewhisperer/tracker/codewhispererTracker.ts
@@ -15,7 +15,8 @@ import { codeWhispererClient } from '../client/codewhisperer'
import { logSendTelemetryEventFailure } from '../../codewhispererChat/controllers/chat/telemetryHelper'
import { Timeout } from '../../shared/utilities/timeoutUtils'
import { getSelectedCustomization } from '../util/customizationUtil'
-import { undefinedIfEmpty } from '../../shared'
+import { isAwsError, undefinedIfEmpty } from '../../shared'
+import { getUnmodifiedAcceptedTokens } from '../util/commonUtil'
/**
* This singleton class is mainly used for calculating the percentage of user modification.
@@ -89,19 +90,20 @@ export class CodeWhispererTracker {
public async emitTelemetryOnSuggestion(suggestion: AcceptedSuggestionEntry | InsertedCode) {
let percentage = 1.0
+ let currString = ''
+ const customizationArn = undefinedIfEmpty(getSelectedCustomization().arn)
try {
if (suggestion.fileUrl?.scheme !== '') {
const document = await vscode.workspace.openTextDocument(suggestion.fileUrl)
if (document) {
- const currString = document.getText(
- new vscode.Range(suggestion.startPosition, suggestion.endPosition)
- )
+ currString = document.getText(new vscode.Range(suggestion.startPosition, suggestion.endPosition))
percentage = this.checkDiff(currString, suggestion.originalString)
}
}
} catch (e) {
getLogger().verbose(`Exception Thrown from CodeWhispererTracker: ${e}`)
+ return
} finally {
if ('conversationID' in suggestion) {
const event: AmazonqModifyCode = {
@@ -120,7 +122,7 @@ export class CodeWhispererTracker {
conversationId: event.cwsprChatConversationId,
messageId: event.cwsprChatMessageId,
modificationPercentage: event.cwsprChatModificationPercentage,
- customizationArn: undefinedIfEmpty(getSelectedCustomization().arn),
+ customizationArn: customizationArn,
},
},
})
@@ -139,9 +141,39 @@ export class CodeWhispererTracker {
codewhispererCharactersAccepted: suggestion.originalString.length,
codewhispererCharactersModified: 0, // TODO: currently we don't have an accurate number for this field with existing implementation
})
- // TODO:
- // Temperary comment out user modification event, need further discussion on how to calculate this metric
- // TelemetryHelper.instance.sendUserModificationEvent(suggestion, percentage)
+
+ codeWhispererClient
+ .sendTelemetryEvent({
+ telemetryEvent: {
+ userModificationEvent: {
+ sessionId: suggestion.sessionId,
+ requestId: suggestion.requestId,
+ programmingLanguage: { languageName: suggestion.language },
+ // deprecated % value and should not be used by service side
+ modificationPercentage: percentage,
+ customizationArn: customizationArn,
+ timestamp: new Date(),
+ acceptedCharacterCount: suggestion.originalString.length,
+ unmodifiedAcceptedCharacterCount: getUnmodifiedAcceptedTokens(
+ suggestion.originalString,
+ currString
+ ),
+ },
+ },
+ })
+ .then()
+ .catch((error) => {
+ let requestId: string | undefined
+ if (isAwsError(error)) {
+ requestId = error.requestId
+ }
+
+ getLogger().debug(
+ `Failed to send UserModificationEvent to CodeWhisperer, requestId: ${requestId ?? ''}, message: ${
+ error.message
+ }`
+ )
+ })
}
}
}
diff --git a/packages/core/src/codewhisperer/util/codeWhispererSession.ts b/packages/core/src/codewhisperer/util/codeWhispererSession.ts
index d6c06ef5350..f57fddaecd0 100644
--- a/packages/core/src/codewhisperer/util/codeWhispererSession.ts
+++ b/packages/core/src/codewhisperer/util/codeWhispererSession.ts
@@ -12,7 +12,7 @@ import {
} from '../../shared/telemetry/telemetry.gen'
import { GenerateRecommendationsRequest, ListRecommendationsRequest, Recommendation } from '../client/codewhisperer'
import { Position } from 'vscode'
-import { CodeWhispererSupplementalContext } from '../models/model'
+import { CodeWhispererSupplementalContext, vsCodeState } from '../models/model'
class CodeWhispererSession {
static #instance: CodeWhispererSession
@@ -41,6 +41,8 @@ class CodeWhispererSession {
fetchCredentialStartTime = 0
sdkApiCallStartTime = 0
invokeSuggestionStartTime = 0
+ timeToFirstRecommendation = 0
+ firstSuggestionShowTime = 0
public static get instance() {
return (this.#instance ??= new CodeWhispererSession())
@@ -58,6 +60,12 @@ class CodeWhispererSession {
}
}
+ setTimeToFirstRecommendation(timeToFirstRecommendation: number) {
+ if (this.invokeSuggestionStartTime) {
+ this.timeToFirstRecommendation = timeToFirstRecommendation - this.invokeSuggestionStartTime
+ }
+ }
+
setSuggestionState(index: number, value: string) {
this.suggestionStates.set(index, value)
}
@@ -75,6 +83,14 @@ class CodeWhispererSession {
return this.completionTypes.get(index) || 'Line'
}
+ getPerceivedLatency(triggerType: CodewhispererTriggerType) {
+ if (triggerType === 'OnDemand') {
+ return this.timeToFirstRecommendation
+ } else {
+ return session.firstSuggestionShowTime - vsCodeState.lastUserModificationTime
+ }
+ }
+
reset() {
this.sessionId = ''
this.requestContext = { request: {} as any, supplementalMetadata: {} as any }
diff --git a/packages/core/src/codewhisperer/util/commonUtil.ts b/packages/core/src/codewhisperer/util/commonUtil.ts
index 201a5f1c595..1d624e77b5e 100644
--- a/packages/core/src/codewhisperer/util/commonUtil.ts
+++ b/packages/core/src/codewhisperer/util/commonUtil.ts
@@ -5,6 +5,7 @@
import * as vscode from 'vscode'
import * as semver from 'semver'
+import { distance } from 'fastest-levenshtein'
import { isCloud9 } from '../../shared/extensionUtilities'
import { getInlineSuggestEnabled } from '../../shared/utilities/editorUtilities'
import {
@@ -76,3 +77,12 @@ export function checkLeftContextKeywordsForJson(fileName: string, leftFileConten
}
return false
}
+
+// With edit distance, complicate usermodification can be considered as simple edit(add, delete, replace),
+// and thus the unmodified part of recommendation length can be deducted/approximated
+// ex. (modified > original): originalRecom: foo -> modifiedRecom: fobarbarbaro, distance = 9, delta = 12 - 9 = 3
+// ex. (modified == original): originalRecom: helloworld -> modifiedRecom: HelloWorld, distance = 2, delta = 10 - 2 = 8
+// ex. (modified < original): originalRecom: CodeWhisperer -> modifiedRecom: CODE, distance = 12, delta = 13 - 12 = 1
+export function getUnmodifiedAcceptedTokens(origin: string, after: string) {
+ return Math.max(origin.length, after.length) - distance(origin, after)
+}
diff --git a/packages/core/src/codewhisperer/util/telemetryHelper.ts b/packages/core/src/codewhisperer/util/telemetryHelper.ts
index 82f812ae1b7..3afb14f2c98 100644
--- a/packages/core/src/codewhisperer/util/telemetryHelper.ts
+++ b/packages/core/src/codewhisperer/util/telemetryHelper.ts
@@ -30,7 +30,6 @@ import { CodeScanRemediationsEventType } from '../client/codewhispereruserclient
export class TelemetryHelper {
// Some variables for client component latency
private sdkApiCallEndTime = 0
- private firstSuggestionShowTime = 0
private allPaginationEndTime = 0
private firstResponseRequestId = ''
// variables for user trigger decision
@@ -41,8 +40,6 @@ export class TelemetryHelper {
private typeAheadLength = 0
private timeSinceLastModification = 0
private lastTriggerDecisionTime = 0
- private invocationTime = 0
- private timeToFirstRecommendation = 0
private classifierResult?: number = undefined
private classifierThreshold?: number = undefined
// variables for tracking end to end sessions
@@ -285,7 +282,7 @@ export class TelemetryHelper {
codewhispererTimeSinceLastUserDecision: this.lastTriggerDecisionTime
? performance.now() - this.lastTriggerDecisionTime
: undefined,
- codewhispererTimeToFirstRecommendation: this.timeToFirstRecommendation,
+ codewhispererTimeToFirstRecommendation: session.timeToFirstRecommendation,
codewhispererTriggerCharacter: autoTriggerType === 'SpecialCharacters' ? this.triggerChar : undefined,
codewhispererSuggestionState: aggregatedSuggestionState,
codewhispererPreviousSuggestionState: this.prevTriggerDecision,
@@ -305,11 +302,11 @@ export class TelemetryHelper {
this.prevTriggerDecision = this.getAggregatedSuggestionState(this.sessionDecisions)
this.lastTriggerDecisionTime = performance.now()
- // When we send a userTriggerDecision of Empty or Discard, we set the time users see the first
- // suggestion to be now.
- let e2eLatency = this.firstSuggestionShowTime - session.invokeSuggestionStartTime
- if (e2eLatency < 0) {
- e2eLatency = performance.now() - session.invokeSuggestionStartTime
+ // When we send a userTriggerDecision for neither Accept nor Reject, service side should not use this value
+ // and client side will set this value to 0.0.
+ let e2eLatency = session.firstSuggestionShowTime - session.invokeSuggestionStartTime
+ if (aggregatedSuggestionState !== 'Reject' && aggregatedSuggestionState !== 'Accept') {
+ e2eLatency = 0.0
}
client
@@ -327,8 +324,11 @@ export class TelemetryHelper {
completionType: this.getSendTelemetryCompletionType(aggregatedCompletionType),
suggestionState: this.getSendTelemetrySuggestionState(aggregatedSuggestionState),
recommendationLatencyMilliseconds: e2eLatency,
+ triggerToResponseLatencyMilliseconds: session.timeToFirstRecommendation,
+ perceivedLatencyMilliseconds: session.getPerceivedLatency(
+ this.sessionDecisions[0].codewhispererTriggerType
+ ),
timestamp: new Date(Date.now()),
- triggerToResponseLatencyMilliseconds: this.timeToFirstRecommendation,
suggestionReferenceCount: referenceCount,
generatedLine: generatedLines,
numberOfRecommendations: suggestionCount,
@@ -377,16 +377,6 @@ export class TelemetryHelper {
this.timeSinceLastModification = timeSinceLastModification
}
- public setInvocationStartTime(invocationTime: number) {
- this.invocationTime = invocationTime
- }
-
- public setTimeToFirstRecommendation(timeToFirstRecommendation: number) {
- if (this.invocationTime) {
- this.timeToFirstRecommendation = timeToFirstRecommendation - this.invocationTime
- }
- }
-
public setTraceId(traceId: string) {
this.traceId = traceId
}
@@ -396,7 +386,7 @@ export class TelemetryHelper {
this.triggerChar = ''
this.typeAheadLength = 0
this.timeSinceLastModification = 0
- this.timeToFirstRecommendation = 0
+ session.timeToFirstRecommendation = 0
this.classifierResult = undefined
this.classifierThreshold = undefined
}
@@ -479,7 +469,7 @@ export class TelemetryHelper {
session.sdkApiCallStartTime = 0
this.sdkApiCallEndTime = 0
session.fetchCredentialStartTime = 0
- this.firstSuggestionShowTime = 0
+ session.firstSuggestionShowTime = 0
this.allPaginationEndTime = 0
this.firstResponseRequestId = ''
}
@@ -503,8 +493,8 @@ export class TelemetryHelper {
}
public setFirstSuggestionShowTime() {
- if (this.firstSuggestionShowTime === 0 && this.sdkApiCallEndTime !== 0) {
- this.firstSuggestionShowTime = performance.now()
+ if (session.firstSuggestionShowTime === 0 && this.sdkApiCallEndTime !== 0) {
+ session.firstSuggestionShowTime = performance.now()
}
}
@@ -517,16 +507,16 @@ export class TelemetryHelper {
// report client component latency after all pagination call finish
// and at least one suggestion is shown to the user
public tryRecordClientComponentLatency() {
- if (this.firstSuggestionShowTime === 0 || this.allPaginationEndTime === 0) {
+ if (session.firstSuggestionShowTime === 0 || this.allPaginationEndTime === 0) {
return
}
telemetry.codewhisperer_clientComponentLatency.emit({
codewhispererRequestId: this.firstResponseRequestId,
codewhispererSessionId: session.sessionId,
codewhispererFirstCompletionLatency: this.sdkApiCallEndTime - session.sdkApiCallStartTime,
- codewhispererEndToEndLatency: this.firstSuggestionShowTime - session.invokeSuggestionStartTime,
+ codewhispererEndToEndLatency: session.firstSuggestionShowTime - session.invokeSuggestionStartTime,
codewhispererAllCompletionsLatency: this.allPaginationEndTime - session.sdkApiCallStartTime,
- codewhispererPostprocessingLatency: this.firstSuggestionShowTime - this.sdkApiCallEndTime,
+ codewhispererPostprocessingLatency: session.firstSuggestionShowTime - this.sdkApiCallEndTime,
codewhispererCredentialFetchingLatency: session.sdkApiCallStartTime - session.fetchCredentialStartTime,
codewhispererPreprocessingLatency: session.fetchCredentialStartTime - session.invokeSuggestionStartTime,
codewhispererCompletionType: 'Line',
From 5f38a7cb6bd6fb7f397ce0e85d8ee0d042ca0ecf Mon Sep 17 00:00:00 2001
From: Nikolas Komonen <118216176+nkomonen-amazon@users.noreply.github.com>
Date: Thu, 10 Oct 2024 15:54:04 -0400
Subject: [PATCH 30/87] fix(telemetry): Crash monitoring fixes (#5741)
## Problem
Crash monitoring is reporting incorrect crash metrics.
This seems to be due to various filesystem errors such as eperm (even
though we were doing an operation on a file we created), enospc (the
user ran out of space on their machine, and other errors.
Because of this we ran in to situations where our state did not reflect
reality, and due to this certain extension
instances were seen as crashed.
## Solution
- Determine if a filesystem is reliable on a machine (try a bunch of
different filesystem flows and ensure nothing throws), if it is THEN we
start the crash monitoring process. Otherwise we do not run it since we
cannot rely it will be accurate.
- We added a `function_call` metric to allow us to determine the ratio
of successes to failures
- Add retries to critical filesystem operations such as the heartbeats
and deleting a crashed extension instance from the state.
- Other various fixes
---
License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---------
Signed-off-by: nkomonen-amazon
---
packages/amazonq/src/extensionNode.ts | 5 +-
packages/core/src/extensionNode.ts | 5 +-
packages/core/src/shared/crashMonitoring.ts | 293 ++++++++++--------
.../core/src/shared/filesystemUtilities.ts | 124 ++++++++
.../src/shared/utilities/functionUtils.ts | 33 +-
packages/core/src/shared/utilities/osUtils.ts | 9 +-
.../src/test/shared/crashMonitoring.test.ts | 18 +-
.../test/shared/filesystemUtilities.test.ts | 9 +
packages/core/src/test/shared/fs/fs.test.ts | 3 +-
.../shared/utilities/functionUtils.test.ts | 83 ++++-
.../src/test/shared/utilities/osUtils.test.ts | 46 +--
packages/core/src/test/testUtil.ts | 6 +-
12 files changed, 463 insertions(+), 171 deletions(-)
diff --git a/packages/amazonq/src/extensionNode.ts b/packages/amazonq/src/extensionNode.ts
index b203e541a84..cd07aeba981 100644
--- a/packages/amazonq/src/extensionNode.ts
+++ b/packages/amazonq/src/extensionNode.ts
@@ -32,7 +32,8 @@ export async function activate(context: vscode.ExtensionContext) {
* the code compatible with web and move it to {@link activateAmazonQCommon}.
*/
async function activateAmazonQNode(context: vscode.ExtensionContext) {
- await (await CrashMonitoring.instance()).start()
+ // Intentionally do not await since this is slow and non-critical
+ void (await CrashMonitoring.instance())?.start()
const extContext = {
extensionContext: context,
@@ -96,5 +97,5 @@ async function setupDevMode(context: vscode.ExtensionContext) {
export async function deactivate() {
// Run concurrently to speed up execution. stop() does not throw so it is safe
- await Promise.all([(await CrashMonitoring.instance()).stop(), deactivateCommon()])
+ await Promise.all([(await CrashMonitoring.instance())?.shutdown(), deactivateCommon()])
}
diff --git a/packages/core/src/extensionNode.ts b/packages/core/src/extensionNode.ts
index f91fa321f59..06185f695cc 100644
--- a/packages/core/src/extensionNode.ts
+++ b/packages/core/src/extensionNode.ts
@@ -78,7 +78,8 @@ export async function activate(context: vscode.ExtensionContext) {
// IMPORTANT: If you are doing setup that should also work in web mode (browser), it should be done in the function below
const extContext = await activateCommon(context, contextPrefix, false)
- await (await CrashMonitoring.instance()).start()
+ // Intentionally do not await since this can be slow and non-critical
+ void (await CrashMonitoring.instance())?.start()
initializeCredentialsProviderManager()
@@ -254,7 +255,7 @@ export async function activate(context: vscode.ExtensionContext) {
export async function deactivate() {
// Run concurrently to speed up execution. stop() does not throw so it is safe
- await Promise.all([await (await CrashMonitoring.instance()).stop(), deactivateCommon()])
+ await Promise.all([await (await CrashMonitoring.instance())?.shutdown(), deactivateCommon()])
await globals.resourceManager.dispose()
}
diff --git a/packages/core/src/shared/crashMonitoring.ts b/packages/core/src/shared/crashMonitoring.ts
index bfd470a7150..00fbb162862 100644
--- a/packages/core/src/shared/crashMonitoring.ts
+++ b/packages/core/src/shared/crashMonitoring.ts
@@ -16,6 +16,8 @@ import nodeFs from 'fs/promises'
import fs from './fs/fs'
import { getLogger } from './logger/logger'
import { crashMonitoringDirNames } from './constants'
+import { throwOnUnstableFileSystem } from './filesystemUtilities'
+import { withRetries } from './utilities/functionUtils'
const className = 'CrashMonitoring'
@@ -26,14 +28,14 @@ const className = 'CrashMonitoring'
*
* - If an extension crashes it cannot report that it crashed.
* - The ExtensionHost is a separate process from the main VS Code editor process where all extensions run in
- * - Read about the [`deactivate()` behavior](../../../../../docs/vscode_behaviors.md)
+ * - Read about the [`deactivate()` behavior](../../../../docs/vscode_behaviors.md)
* - An IDE instance is one instance of VS Code, and Extension Instance is 1 instance of our extension. These are 1:1.
*
* ### How it works at a high level:
*
- * - Each IDE instance will start its own crash reporting process on startup
- * - The crash reporting process works with each instance sending heartbeats to a centralized state. Separately each instance
- * has a "Checker" the each entry in the centralized to see if it is not running anymore, and appropriately handles when needed.
+ * - Each IDE instance will start its own crash monitoring process on startup
+ * - The crash monitoring process works with each instance sending heartbeats to a centralized state. Separately each instance
+ * has a "Checker" that checks each heartbeat to see if it is not running anymore, and appropriately handles when needed.
*
* - On a crash we will emit a `session_end` metrics with `{ result: 'Failed', reason: 'ExtHostCrashed', crashedSessionId: '...' }`
* - On successful shutdown a `session_end` with a successful result is already emitted elsewhere.
@@ -42,18 +44,22 @@ const className = 'CrashMonitoring'
*
* - To get the most verbose debug logs, configure the devmode setting: `crashReportInterval`
*
+ * - This entire feature is non critical and should not impede extension usage if something goes wrong. As a result, we
+ * swallow all errors and only log/telemetry issues. This is the reason for all the try/catch statements
+ *
* ### Limitations
* - We will never truly know if we are the cause of the crash
* - Since all extensions run in the same Ext Host process, any one of them could cause it to crash and we wouldn't be
* able to differentiate
* - If the IDE itself crashes, unrelated to the extensions, it will still be seen as a crash in our telemetry
* - We are not able to explicitly determine if we were the cause of the crash
- * - If the user shuts down their computer after a crash before the next interval of the Primary can run, that info is lost
+ * - If the user shuts down their computer after a crash before the next crash check can run, that info is lost
* - We cannot persist crash information on computer restart
+ * - We use the users filesystem to maintain the state of running extension instances, but the
+ * filesystem is not reliable and can lead to incorrect crash reports
+ * - To mitigate this we do not run crash reporting on machines that we detect have a flaky filesystem
*/
export class CrashMonitoring {
- private isStarted: boolean = false
-
protected heartbeat: Heartbeat | undefined
protected crashChecker: CrashChecker | undefined
@@ -65,17 +71,32 @@ export class CrashMonitoring {
private readonly devLogger: Logger | undefined
) {}
+ static #didTryCreate = false
static #instance: CrashMonitoring | undefined
- public static async instance(): Promise {
- const isDevMode = getIsDevMode()
- const devModeLogger: Logger | undefined = isDevMode ? getLogger() : undefined
- return (this.#instance ??= new CrashMonitoring(
- await crashMonitoringStateFactory(),
- DevSettings.instance.get('crashCheckInterval', 1000 * 60 * 3),
- isDevMode,
- isAutomation(),
- devModeLogger
- ))
+ /** Returns an instance of this class or undefined if any initial validation fails. */
+ public static async instance(): Promise {
+ // Since the first attempt to create an instance may have failed, we do not
+ // attempt to create an instance again and return whatever we have
+ if (this.#didTryCreate === true) {
+ return this.#instance
+ }
+
+ try {
+ this.#didTryCreate = true
+ const isDevMode = getIsDevMode()
+ const devModeLogger: Logger | undefined = isDevMode ? getLogger() : undefined
+ const state = await crashMonitoringStateFactory() // can throw
+ return (this.#instance ??= new CrashMonitoring(
+ state,
+ DevSettings.instance.get('crashCheckInterval', 1000 * 60 * 10), // check every 10 minutes
+ isDevMode,
+ isAutomation(),
+ devModeLogger
+ ))
+ } catch (error) {
+ emitFailure({ functionName: 'instance', error })
+ return undefined
+ }
}
/** Start the Crash Monitoring process */
@@ -84,23 +105,25 @@ export class CrashMonitoring {
return
}
- // In the Prod code this runs by default and interferes as it reports its own heartbeats.
+ // During tests, the Prod code also runs this function. It interferes with telemetry assertion since it reports additional heartbeats.
if (this.isAutomation) {
return
}
- // Dont throw since this feature is not critical and shouldn't prevent extension execution
try {
this.heartbeat = new Heartbeat(this.state, this.checkInterval, this.isDevMode)
this.crashChecker = new CrashChecker(this.state, this.checkInterval, this.isDevMode, this.devLogger)
await this.heartbeat.start()
await this.crashChecker.start()
-
- this.isStarted = true
} catch (error) {
emitFailure({ functionName: 'start', error })
- // In development this gives us a useful stacktrace
+ try {
+ this.crashChecker?.cleanup()
+ await this.heartbeat?.cleanup()
+ } catch {}
+
+ // Surface errors during development, otherwise it can be missed.
if (this.isDevMode) {
throw error
}
@@ -108,40 +131,21 @@ export class CrashMonitoring {
}
/** Stop the Crash Monitoring process, signifying a graceful shutdown */
- public async stop() {
- if (!this.isStarted) {
- return
- }
-
- // Dont throw since this feature is not critical and shouldn't prevent extension shutdown
+ public async shutdown() {
try {
- this.crashChecker?.stop()
- await this.heartbeat?.stop()
+ this.crashChecker?.cleanup()
+ await this.heartbeat?.shutdown()
} catch (error) {
try {
// This probably wont emit in time before shutdown, but may be written to the logs
emitFailure({ functionName: 'stop', error })
- } catch (e) {
- // In case emit fails, do nothing
- }
+ } catch {}
+
if (this.isDevMode) {
throw error
}
}
}
-
- /**
- * Mimic a crash of the extension, or can just be used as cleanup.
- * Only use this for tests.
- */
- protected crash() {
- if (!this.isStarted) {
- return
- }
-
- this.crashChecker?.stop()
- this.heartbeat?.crash()
- }
}
/**
@@ -149,7 +153,6 @@ export class CrashMonitoring {
* {@link CrashChecker} listens for these.
*/
class Heartbeat {
- private isRunning: boolean = false
private intervalRef: NodeJS.Timer | undefined
constructor(
private readonly state: FileSystemState,
@@ -158,9 +161,6 @@ class Heartbeat {
) {}
public async start() {
- this.isRunning = true
-
- // heartbeat 2 times per check
const heartbeatInterval = this.checkInterval / 2
// Send an initial heartbeat immediately
@@ -171,14 +171,11 @@ class Heartbeat {
try {
await this.state.sendHeartbeat()
} catch (e) {
- emitFailure({ functionName: 'sendHeartbeat', error: e })
-
- // Since there was an error we want to stop crash monitoring since it is pointless.
- // We will need to monitor telemetry to see if we can determine widespread issues.
- // Make sure it is signaled as a graceful shutdown to reduce noise of crashed extensions.
- await this.stop()
+ try {
+ await this.cleanup()
+ emitFailure({ functionName: 'sendHeartbeatInterval', error: e })
+ } catch {}
- // During development we are fine with impacting extension execution, so throw
if (this.isDevMode) {
throw e
}
@@ -186,35 +183,37 @@ class Heartbeat {
}, heartbeatInterval)
}
- public async stop() {
- // non-happy path where heartbeats were never started.
- if (!this.isRunning) {
- return
- }
-
+ /** Stops everything, signifying a graceful shutdown */
+ public async shutdown() {
globals.clock.clearInterval(this.intervalRef)
return this.state.indicateGracefulShutdown()
}
- public crash() {
+ /**
+ * Safely attempts to clean up this heartbeat from the state to try and avoid
+ * an incorrectly indicated crash. Use this on failures.
+ *
+ * ---
+ *
+ * IMPORTANT: This function must not throw as this function is run within a catch
+ */
+ public async cleanup() {
+ try {
+ await this.shutdown()
+ } catch {}
+ try {
+ await this.state.clearHeartbeat()
+ } catch {}
+ }
+
+ /** Mimics a crash, only for testing */
+ public testCrash() {
globals.clock.clearInterval(this.intervalRef)
}
}
/**
- * This checks for if an extension has crashed and handles that result appropriately.
- * It listens to heartbeats sent by {@link Heartbeat}, and then handles appropriately when the heartbeats
- * stop.
- *
- * ---
- *
- * This follows the Primary/Secondary design where one of the extension instances is the Primary checker
- * and all others are Secondary.
- *
- * The Primary actually reads the state and reports crashes if detected.
- *
- * The Secondary continuously attempts to become the Primary if the previous Primary is no longer responsive.
- * This helps to reduce raceconditions for operations on the state.
+ * This checks the heartbeats of each known extension to see if it has crashed and handles that result appropriately.
*/
class CrashChecker {
private intervalRef: NodeJS.Timer | undefined
@@ -235,17 +234,14 @@ class CrashChecker {
tryCheckCrash(this.state, this.checkInterval, this.isDevMode, this.devLogger)
)
+ // check on an interval
this.intervalRef = globals.clock.setInterval(async () => {
try {
await tryCheckCrash(this.state, this.checkInterval, this.isDevMode, this.devLogger)
} catch (e) {
emitFailure({ functionName: 'checkCrashInterval', error: e })
+ this.cleanup()
- // Since there was an error we want to stop crash monitoring since it is pointless.
- // We will need to monitor telemetry to see if we can determine widespread issues.
- this.stop()
-
- // During development we are fine with impacting extension execution, so throw
if (this.isDevMode) {
throw e
}
@@ -272,13 +268,13 @@ class CrashChecker {
// Ext is not running anymore, handle appropriately depending on why it stopped running
await state.handleExtNotRunning(ext, {
- shutdown: async () => {
+ onShutdown: async () => {
// Nothing to do, just log info if necessary
devLogger?.debug(
`crashMonitoring: SHUTDOWN: following has gracefully shutdown: pid ${ext.extHostPid} + sessionId: ${ext.sessionId}`
)
},
- crash: async () => {
+ onCrash: async () => {
// Debugger instances may incorrectly look like they crashed, so don't emit.
// Example is if I hit the red square in the debug menu, it is a non-graceful shutdown. But the regular
// 'x' button in the Debug IDE instance is a graceful shutdown.
@@ -317,16 +313,17 @@ class CrashChecker {
function isStoppedHeartbeats(ext: ExtInstanceHeartbeat, checkInterval: number) {
const millisSinceLastHeartbeat = globals.clock.Date.now() - ext.lastHeartbeat
- // since heartbeats happen 2 times per check interval it will have occured
- // at least once in the timespan of the check interval.
- //
- // But if we want to be more flexible this condition can be modified since
- // something like global state taking time to sync can return the incorrect last heartbeat value.
return millisSinceLastHeartbeat >= checkInterval
}
}
- public stop() {
+ /** Use this on failures to terminate the crash checker */
+ public cleanup() {
+ globals.clock.clearInterval(this.intervalRef)
+ }
+
+ /** Mimics a crash, only for testing */
+ public testCrash() {
globals.clock.clearInterval(this.intervalRef)
}
}
@@ -358,6 +355,11 @@ function getDefaultDependencies(): MementoStateDependencies {
devLogger: getIsDevMode() ? getLogger() : undefined,
}
}
+/**
+ * Factory to create an instance of the state.
+ *
+ * @throws if the filesystem state cannot be confirmed to be stable, i.e flaky fs operations
+ */
export async function crashMonitoringStateFactory(deps = getDefaultDependencies()): Promise {
const state: FileSystemState = new FileSystemState(deps)
await state.init()
@@ -365,14 +367,16 @@ export async function crashMonitoringStateFactory(deps = getDefaultDependencies(
}
/**
- * The state of all running extensions. This state is globally shared with all other extension instances.
- * This state specifically uses the File System.
+ * The state of all running extensions.
+ * - is globally shared with all other extension instances.
+ * - uses the File System
+ * - is not truly reliable since filesystems are not reliable
*/
export class FileSystemState {
private readonly stateDirPath: string
/**
- * Use {@link crashMonitoringStateFactory} to make an instance
+ * IMORTANT: Use {@link crashMonitoringStateFactory} to make an instance
*/
constructor(protected readonly deps: MementoStateDependencies) {
this.stateDirPath = path.join(this.deps.workDirPath, crashMonitoringDirNames.root)
@@ -385,8 +389,18 @@ export class FileSystemState {
/**
* Does the required initialization steps, this must always be run after
* creation of the instance.
+ *
+ * @throws if the filesystem state cannot be confirmed to be stable, i.e flaky fs operations
*/
public async init() {
+ // IMPORTANT: do not run crash reporting on unstable filesystem to reduce invalid crash data
+ //
+ // NOTE: Emits a metric to know how many clients we skipped
+ await telemetry.function_call.run(async (span) => {
+ span.record({ className, functionName: 'FileSystemStateValidation' })
+ await withFailCtx('validateFileSystemStability', () => throwOnUnstableFileSystem())
+ })
+
// Clear the state if the user did something like a computer restart
if (await this.deps.isStateStale()) {
await this.clearState()
@@ -395,17 +409,31 @@ export class FileSystemState {
// ------------------ Heartbeat methods ------------------
public async sendHeartbeat() {
- await withFailCtx('sendHeartbeatState', async () => {
- const dir = await this.runningExtsDir()
- const extId = this.createExtId(this.ext)
- await fs.writeFile(
- path.join(dir, extId),
- JSON.stringify({ ...this.ext, lastHeartbeat: this.deps.now() }, undefined, 4)
- )
- this.deps.devLogger?.debug(
- `crashMonitoring: HEARTBEAT pid ${this.deps.pid} + sessionId: ${this.deps.sessionId.slice(0, 8)}-...`
- )
- })
+ const extId = this.createExtId(this.ext)
+
+ try {
+ const func = async () => {
+ const dir = await this.runningExtsDir()
+ await fs.writeFile(
+ path.join(dir, extId),
+ JSON.stringify({ ...this.ext, lastHeartbeat: this.deps.now() }, undefined, 4)
+ )
+ this.deps.devLogger?.debug(
+ `crashMonitoring: HEARTBEAT pid ${this.deps.pid} + sessionId: ${this.deps.sessionId.slice(0, 8)}-...`
+ )
+ }
+ const funcWithCtx = () => withFailCtx('sendHeartbeatState', func)
+ const funcWithRetries = withRetries(funcWithCtx, { maxRetries: 8, delay: 100, backoff: 2 })
+ return await funcWithRetries
+ } catch (e) {
+ // delete this ext from the state to avoid an incorrectly reported crash since we could not send a new heartbeat
+ await withFailCtx('sendHeartbeatFailureCleanup', () => this.clearHeartbeat())
+ throw e
+ }
+ }
+ /** Clears this extentions heartbeat from the state */
+ public async clearHeartbeat() {
+ await this.deleteHeartbeatFile(this.extId)
}
/**
@@ -435,26 +463,34 @@ export class FileSystemState {
*/
public async handleExtNotRunning(
ext: ExtInstance,
- opts: { shutdown: () => Promise; crash: () => Promise }
+ opts: { onShutdown: () => Promise; onCrash: () => Promise }
): Promise {
const extId = this.createExtId(ext)
const shutdownFilePath = path.join(await this.shutdownExtsDir(), extId)
if (await withFailCtx('existsShutdownFile', () => fs.exists(shutdownFilePath))) {
- await opts.shutdown()
+ await opts.onShutdown()
// We intentionally do not clean up the file in shutdown since there may be another
// extension may be doing the same thing in parallel, and would read the extension as
// crashed since the file was missing. The file will be cleared on computer restart though.
// TODO: Be smart and clean up the file after some time.
} else {
- await opts.crash()
+ await opts.onCrash()
}
- // Clean up the running extension file since it is no longer exists
+ // Clean up the running extension file since it no longer exists
+ await this.deleteHeartbeatFile(extId)
+ }
+ public async deleteHeartbeatFile(extId: ExtInstanceId) {
const dir = await this.runningExtsDir()
- // Use force since another checker may have already removed this file before this is ran
- await withFailCtx('deleteStaleRunningFile', () => fs.delete(path.join(dir, extId), { force: true }))
+ // Retry file deletion to prevent incorrect crash reports. Common Windows errors seen in telemetry: EPERM/EBUSY.
+ // See: https://github.com/aws/aws-toolkit-vscode/pull/5335
+ await withRetries(() => withFailCtx('deleteStaleRunningFile', () => fs.delete(path.join(dir, extId))), {
+ maxRetries: 8,
+ delay: 100,
+ backoff: 2,
+ })
}
// ------------------ State data ------------------
@@ -482,7 +518,7 @@ export class FileSystemState {
private async runningExtsDir(): Promise {
const p = path.join(this.stateDirPath, crashMonitoringDirNames.running)
// ensure the dir exists
- await withFailCtx('ensureRunningExtsDir', () => fs.mkdir(p))
+ await withFailCtx('ensureRunningExtsDir', () => nodeFs.mkdir(p, { recursive: true }))
return p
}
private async shutdownExtsDir() {
@@ -492,7 +528,11 @@ export class FileSystemState {
return p
}
public async clearState(): Promise {
- await withFailCtx('clearState', async () => fs.delete(this.stateDirPath, { force: true }))
+ this.deps.devLogger?.debug('crashMonitoring: CLEAR_STATE: Started')
+ await withFailCtx('clearState', async () => {
+ await fs.delete(this.stateDirPath, { force: true, recursive: true })
+ this.deps.devLogger?.debug('crashMonitoring: CLEAR_STATE: Succeeded')
+ })
}
public async getAllExts(): Promise {
const res = await withFailCtx('getAllExts', async () => {
@@ -504,17 +544,22 @@ export class FileSystemState {
const allExts = allExtIds.map>(async (extId: string) => {
// Due to a race condition, a separate extension instance may have removed this file by this point. It is okay since
// we will assume that other instance handled its termination appropriately.
- const ext = await withFailCtx('parseRunningExtFile', async () =>
- ignoreBadFileError(async () => {
- const text = await fs.readFileText(path.join(await this.runningExtsDir(), extId))
-
- if (!text) {
- return undefined
- }
-
- // This was sometimes throwing SyntaxError
- return JSON.parse(text) as ExtInstanceHeartbeat
- })
+ // NOTE: On Windows we were failing on EBUSY, so we retry on failure.
+ const ext: ExtInstanceHeartbeat | undefined = await withRetries(
+ () =>
+ withFailCtx('parseRunningExtFile', async () =>
+ ignoreBadFileError(async () => {
+ const text = await fs.readFileText(path.join(await this.runningExtsDir(), extId))
+
+ if (!text) {
+ return undefined
+ }
+
+ // This was sometimes throwing SyntaxError
+ return JSON.parse(text) as ExtInstanceHeartbeat
+ })
+ ),
+ { maxRetries: 6, delay: 100, backoff: 2 }
)
if (ext === undefined) {
@@ -591,7 +636,7 @@ async function withFailCtx(ctx: string, fn: () => Promise): Promise {
// make sure we await the function so it actually executes within the try/catch
return await fn()
} catch (err) {
- throw CrashMonitoringError.chain(err, `Failed "${ctx}"`, { code: className })
+ throw CrashMonitoringError.chain(err, `Context: "${ctx}"`, { code: className })
}
}
diff --git a/packages/core/src/shared/filesystemUtilities.ts b/packages/core/src/shared/filesystemUtilities.ts
index 351cd7028b5..b216427634e 100644
--- a/packages/core/src/shared/filesystemUtilities.ts
+++ b/packages/core/src/shared/filesystemUtilities.ts
@@ -11,6 +11,8 @@ import { getLogger } from './logger'
import * as pathutils from './utilities/pathUtils'
import globals from '../shared/extensionGlobals'
import fs from '../shared/fs/fs'
+import { ToolkitError } from './errors'
+import * as nodeFs from 'fs/promises'
export const tempDirPath = path.join(
// https://github.com/aws/aws-toolkit-vscode/issues/240
@@ -246,3 +248,125 @@ export async function setDefaultDownloadPath(downloadPath: string) {
getLogger().error('Error while setting "aws.downloadPath"', err as Error)
}
}
+
+const FileSystemStabilityExceptionId = 'FileSystemStabilityException'
+const FileSystemStabilityException = ToolkitError.named(FileSystemStabilityExceptionId)
+
+/**
+ * Run this function to validate common file system calls/flows, ensuring they work
+ * as expected.
+ *
+ * The intent of this function is to catch potential fs issues early,
+ * testing common cases of fs usage. We need this since each machine can behave
+ * differently depending on things like OS, permissions, disk speed, ...
+ *
+ * @throws a {@link FileSystemStabilityException} which wraps the underlying failed fs operation error.
+ */
+export async function throwOnUnstableFileSystem(tmpRoot: string = tempDirPath) {
+ const tmpFolder = path.join(tmpRoot, `validateStableFS-${crypto.randomBytes(4).toString('hex')}`)
+ const tmpFile = path.join(tmpFolder, 'file.txt')
+
+ try {
+ // test basic folder operations
+ await withFailCtx('mkdirInitial', () => fs.mkdir(tmpFolder))
+ // Verifies that we do not throw if the dir exists and we try to make it again
+ await withFailCtx('mkdirButAlreadyExists', () => fs.mkdir(tmpFolder))
+ // Test subfolder creation. Based on telemetry it looks like the vsc mkdir may be flaky
+ // when creating subfolders. Hopefully this gives us some useful information.
+ const subfolderRoot = path.join(tmpFolder, 'a')
+ const subfolderPath = path.join(subfolderRoot, 'b/c/d/e')
+ await withFailCtx('mkdirSubfolderNode', () => nodeFs.mkdir(subfolderPath, { recursive: true }))
+ await withFailCtx('rmdirInitialForNode', () => fs.delete(subfolderRoot, { recursive: true }))
+ await withFailCtx('mkdirSubfolderVsc', () => fs.mkdir(subfolderPath))
+ await withFailCtx('rmdirInitialForVsc', () => fs.delete(subfolderRoot, { recursive: true }))
+
+ // test basic file operations
+ await withFailCtx('mkdirForFileOpsTest', () => fs.mkdir(tmpFolder))
+ await withFailCtx('writeFile1', () => fs.writeFile(tmpFile, 'test1'))
+ await withFailCtx('readFile1', async () => {
+ const text = await fs.readFileText(tmpFile)
+ if (text !== 'test1') {
+ throw new Error(`Unexpected file contents: "${text}"`)
+ }
+ })
+ // overwrite the file content multiple times
+ await withFailCtx('writeFile2', () => fs.writeFile(tmpFile, 'test2'))
+ await withFailCtx('writeFile3', () => fs.writeFile(tmpFile, 'test3'))
+ await withFailCtx('writeFile4', () => fs.writeFile(tmpFile, 'test4'))
+ await withFailCtx('writeFile5', () => fs.writeFile(tmpFile, 'test5'))
+ await withFailCtx('readFile5', async () => {
+ const text = await fs.readFileText(tmpFile)
+ if (text !== 'test5') {
+ throw new Error(`Unexpected file contents after multiple writes: "${text}"`)
+ }
+ })
+ // write a large file, ensuring we are not near a space limit
+ await withFailCtx('writeFileLarge', () => fs.writeFile(tmpFile, 'a'.repeat(1000)))
+ // test concurrent reads on a file
+ await withFailCtx('writeFileConcurrencyTest', () => fs.writeFile(tmpFile, 'concurrencyTest'))
+ const result = await Promise.all([
+ withFailCtx('readFileConcurrent1', () => fs.readFileText(tmpFile)),
+ withFailCtx('readFileConcurrent2', () => fs.readFileText(tmpFile)),
+ withFailCtx('readFileConcurrent3', () => fs.readFileText(tmpFile)),
+ withFailCtx('readFileConcurrent4', () => fs.readFileText(tmpFile)),
+ withFailCtx('readFileConcurrent5', () => fs.readFileText(tmpFile)),
+ ])
+ if (result.some((text) => text !== 'concurrencyTest')) {
+ throw new Error(`Unexpected concurrent file reads: ${result}`)
+ }
+ // test deleting a file
+ await withFailCtx('deleteFileInitial', () => fs.delete(tmpFile))
+ await withFailCtx('writeFileAfterDelete', () => fs.writeFile(tmpFile, 'afterDelete'))
+ await withFailCtx('readNewFileAfterDelete', async () => {
+ const text = await fs.readFileText(tmpFile)
+ if (text !== 'afterDelete') {
+ throw new Error(`Unexpected file content after writing to deleted file: "${text}"`)
+ }
+ })
+ await withFailCtx('deleteFileFully', () => fs.delete(tmpFile))
+ await withFailCtx('notExistsFileAfterDelete', async () => {
+ const res = await fs.exists(tmpFile)
+ if (res) {
+ throw new Error(`Expected file to NOT exist: "${tmpFile}"`)
+ }
+ })
+
+ // test rename
+ await withFailCtx('writeFileForRename', () => fs.writeFile(tmpFile, 'TestingRename'))
+ const tmpFileRenamed = tmpFile + '.renamed'
+ await withFailCtx('renameFile', () => fs.rename(tmpFile, tmpFileRenamed))
+ await withFailCtx('existsRenamedFile', async () => {
+ const res = await fs.exists(tmpFileRenamed)
+ if (!res) {
+ throw new Error(`Expected RENAMED file to exist: "${tmpFileRenamed}"`)
+ }
+ })
+ await withFailCtx('writeToRenamedFile', async () => fs.writeFile(tmpFileRenamed, 'hello'))
+ await withFailCtx('readFromRenamedFile', async () => {
+ const res = await fs.readFileText(tmpFileRenamed)
+ if (res !== 'hello') {
+ throw new Error(`Expected RENAMED file to be writable: "${tmpFileRenamed}"`)
+ }
+ })
+ await withFailCtx('renameFileReset', () => fs.rename(tmpFileRenamed, tmpFile))
+ await withFailCtx('renameFileResetExists', async () => {
+ const res = await fs.exists(tmpFile)
+ if (!res) {
+ throw new Error(`Expected reverted renamed file to exist: "${tmpFile}"`)
+ }
+ })
+ } finally {
+ await fs.delete(tmpFolder, { recursive: true, force: true })
+ }
+
+ async function withFailCtx(ctx: string, fn: () => Promise): Promise {
+ try {
+ return await fn()
+ } catch (e) {
+ if (!(e instanceof Error)) {
+ throw e
+ }
+ throw FileSystemStabilityException.chain(e, `context: "${ctx}"`, { code: FileSystemStabilityExceptionId })
+ }
+ }
+}
diff --git a/packages/core/src/shared/utilities/functionUtils.ts b/packages/core/src/shared/utilities/functionUtils.ts
index d21727bac1d..f61a3abde34 100644
--- a/packages/core/src/shared/utilities/functionUtils.ts
+++ b/packages/core/src/shared/utilities/functionUtils.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { Timeout } from './timeoutUtils'
+import { sleep, Timeout } from './timeoutUtils'
/**
* Creates a function that always returns a 'shared' Promise.
@@ -145,3 +145,34 @@ export function cancellableDebounce(
cancel: cancel,
}
}
+
+/**
+ * Executes the given function, retrying if it throws.
+ *
+ * @param opts - if no opts given, defaults are used
+ */
+export async function withRetries(
+ fn: () => Promise,
+ opts?: { maxRetries?: number; delay?: number; backoff?: number }
+): Promise {
+ const maxRetries = opts?.maxRetries ?? 3
+ const delay = opts?.delay ?? 0
+ const backoff = opts?.backoff ?? 1
+
+ let retryCount = 0
+ let latestDelay = delay
+ while (true) {
+ try {
+ return await fn()
+ } catch (err) {
+ retryCount++
+ if (retryCount >= maxRetries) {
+ throw err
+ }
+ if (latestDelay > 0) {
+ await sleep(latestDelay)
+ latestDelay = latestDelay * backoff
+ }
+ }
+ }
+}
diff --git a/packages/core/src/shared/utilities/osUtils.ts b/packages/core/src/shared/utilities/osUtils.ts
index b0266f73e1a..2ed7b976998 100644
--- a/packages/core/src/shared/utilities/osUtils.ts
+++ b/packages/core/src/shared/utilities/osUtils.ts
@@ -15,12 +15,13 @@ import * as os from 'os'
* Use this function to perform one-time initialization tasks that should only happen
* once per OS session, regardless of how many extension instances are running.
*/
-export async function isNewOsSession(now = () => globals.clock.Date.now(), uptime = () => os.uptime()) {
+export async function isNewOsSession(now = () => globals.clock.Date.now(), uptimeMillis = () => os.uptime() * 1000) {
// Windows does not have an ephemeral /tmp/ folder that deletes on shutdown, while unix-like os's do.
// So in Windows we calculate the start time and see if it changed from the previous known start time.
const lastStartTime = globals.globalState.tryGet('lastOsStartTime', Number)
- // uptime() returns seconds, convert to ms
- const currentOsStartTime = now() - uptime() * 1000 * 60
+
+ const uptime = uptimeMillis()
+ const currentOsStartTime = now() - uptime
if (lastStartTime === undefined) {
await globals.globalState.update('lastOsStartTime', currentOsStartTime)
@@ -28,7 +29,7 @@ export async function isNewOsSession(now = () => globals.clock.Date.now(), uptim
}
// If the current start time is later than the last, it means we are in a new session since they should be the same value.
- // But to account for small differences in how the current time is calculate, we add in a 5 second buffer.
+ // But to account for minor millisecond differnces, we add in a 5 second buffer.
if (currentOsStartTime - 1000 * 5 > lastStartTime) {
await globals.globalState.update('lastOsStartTime', currentOsStartTime)
return true
diff --git a/packages/core/src/test/shared/crashMonitoring.test.ts b/packages/core/src/test/shared/crashMonitoring.test.ts
index ec61d5487f3..5802592ea8c 100644
--- a/packages/core/src/test/shared/crashMonitoring.test.ts
+++ b/packages/core/src/test/shared/crashMonitoring.test.ts
@@ -14,8 +14,10 @@ class TestCrashMonitoring extends CrashMonitoring {
public constructor(...deps: ConstructorParameters) {
super(...deps)
}
- public override crash() {
- super.crash()
+ /** Imitates an extension crash */
+ public async crash() {
+ this.crashChecker?.testCrash()
+ this.heartbeat?.testCrash()
}
}
@@ -87,7 +89,7 @@ export const crashMonitoringTest = async () => {
// Ext 1 does a graceful shutdown
await exts[1].ext.start()
- await exts[1].ext.stop()
+ await exts[1].ext.shutdown()
await awaitIntervals(oneInterval)
// Ext 1 did a graceful shutdown so no metric emitted
assertTelemetry('session_end', [])
@@ -99,7 +101,7 @@ export const crashMonitoringTest = async () => {
const exts = await makeTestExtensions(2)
await exts[0].ext.start()
- exts[0].ext.crash()
+ await exts[0].ext.crash()
await awaitIntervals(oneInterval)
// There is no other active instance to report the issue
assertTelemetry('session_end', [])
@@ -121,7 +123,7 @@ export const crashMonitoringTest = async () => {
}
for (let i = 1; i < extCount; i++) {
- exts[i].ext.crash()
+ await exts[i].ext.crash()
latestCrashedExts.push(exts[i])
}
@@ -142,7 +144,7 @@ export const crashMonitoringTest = async () => {
// start Ext 1 then crash it, Ext 0 finds the crash
await exts[1].ext.start()
- exts[1].ext.crash()
+ await exts[1].ext.crash()
latestCrashedExts.push(exts[1])
await awaitIntervals(oneInterval * 1)
@@ -150,14 +152,14 @@ export const crashMonitoringTest = async () => {
// start Ext 2 and crash Ext 0, Ext 2 is promoted to Primary checker
await exts[2].ext.start()
- exts[0].ext.crash()
+ await exts[0].ext.crash()
latestCrashedExts.push(exts[0])
await awaitIntervals(oneInterval * 1)
assertCrashedExtensions(latestCrashedExts)
// Ext 3 starts, then crashes. Ext 2 reports the crash since it is the Primary checker
await exts[3].ext.start()
- exts[3].ext.crash()
+ await exts[3].ext.crash()
latestCrashedExts.push(exts[3])
await awaitIntervals(oneInterval * 1)
assertCrashedExtensions(latestCrashedExts)
diff --git a/packages/core/src/test/shared/filesystemUtilities.test.ts b/packages/core/src/test/shared/filesystemUtilities.test.ts
index b99e8f4b0bf..3bb3a018b37 100644
--- a/packages/core/src/test/shared/filesystemUtilities.test.ts
+++ b/packages/core/src/test/shared/filesystemUtilities.test.ts
@@ -13,8 +13,10 @@ import {
isInDirectory,
makeTemporaryToolkitFolder,
tempDirPath,
+ throwOnUnstableFileSystem,
} from '../../shared/filesystemUtilities'
import { fs } from '../../shared'
+import { TestFolder } from '../testUtil'
describe('filesystemUtilities', function () {
const targetFilename = 'findThisFile12345.txt'
@@ -191,4 +193,11 @@ describe('filesystemUtilities', function () {
assert.strictEqual(actual, 3)
})
})
+
+ describe('throwOnUnstableFileSystem', async function () {
+ it('does not throw on stable filesystem', async function () {
+ const testFolder = await TestFolder.create()
+ await throwOnUnstableFileSystem(testFolder.path)
+ })
+ })
})
diff --git a/packages/core/src/test/shared/fs/fs.test.ts b/packages/core/src/test/shared/fs/fs.test.ts
index b60ba2760fc..5fa4428559f 100644
--- a/packages/core/src/test/shared/fs/fs.test.ts
+++ b/packages/core/src/test/shared/fs/fs.test.ts
@@ -340,6 +340,7 @@ describe('FileSystem', function () {
it('deletes directory with recursive:true', async function () {
const dir = await testFolder.mkdir()
+ await testFolder.write('testfile.txt', 'testText')
await fs.delete(dir, { recursive: true })
assert(!existsSync(dir))
})
@@ -348,7 +349,7 @@ describe('FileSystem', function () {
const dir = await testFolder.mkdir()
const f = path.join(dir, 'missingfile.txt')
assert(!existsSync(f))
- await fs.delete(f, { recursive: true })
+ await fs.delete(f)
})
it('error if file *and* its parent dir not found', async function () {
diff --git a/packages/core/src/test/shared/utilities/functionUtils.test.ts b/packages/core/src/test/shared/utilities/functionUtils.test.ts
index 43da4ebb619..fb87cf3501e 100644
--- a/packages/core/src/test/shared/utilities/functionUtils.test.ts
+++ b/packages/core/src/test/shared/utilities/functionUtils.test.ts
@@ -4,8 +4,10 @@
*/
import assert from 'assert'
-import { once, onceChanged, debounce } from '../../../shared/utilities/functionUtils'
+import { once, onceChanged, debounce, withRetries } from '../../../shared/utilities/functionUtils'
import { installFakeClock } from '../../testUtil'
+import { stub, SinonStub } from 'sinon'
+import { InstalledClock } from '@sinonjs/fake-timers'
describe('functionUtils', function () {
it('once()', function () {
@@ -107,3 +109,82 @@ describe('debounce', function () {
})
})
})
+
+// function to test the withRetries method. It passes in a stub function as the argument and has different tests that throw on different iterations
+describe('withRetries', function () {
+ let clock: InstalledClock
+ let fn: SinonStub
+
+ beforeEach(function () {
+ fn = stub()
+ clock = installFakeClock()
+ })
+
+ afterEach(function () {
+ clock.uninstall()
+ })
+
+ it('retries the function until it succeeds, using defaults', async function () {
+ fn.onCall(0).throws()
+ fn.onCall(1).throws()
+ fn.onCall(2).resolves('success')
+ assert.strictEqual(await withRetries(fn), 'success')
+ })
+
+ it('retries the function until it succeeds at the final try', async function () {
+ fn.onCall(0).throws()
+ fn.onCall(1).throws()
+ fn.onCall(2).throws()
+ fn.onCall(3).resolves('success')
+ assert.strictEqual(await withRetries(fn, { maxRetries: 4 }), 'success')
+ })
+
+ it('throws the last error if the function always fails, using defaults', async function () {
+ fn.onCall(0).throws()
+ fn.onCall(1).throws()
+ fn.onCall(2).throws()
+ fn.onCall(3).resolves('unreachable')
+ await assert.rejects(async () => {
+ await withRetries(fn)
+ })
+ })
+
+ it('throws the last error if the function always fails', async function () {
+ fn.onCall(0).throws()
+ fn.onCall(1).throws()
+ fn.onCall(2).throws()
+ fn.onCall(3).throws()
+ fn.onCall(4).resolves('unreachable')
+ await assert.rejects(async () => {
+ await withRetries(fn, { maxRetries: 4 })
+ })
+ })
+
+ it('honors retry delay + backoff multiplier', async function () {
+ fn.onCall(0).throws() // 100ms
+ fn.onCall(1).throws() // 200ms
+ fn.onCall(2).throws() // 400ms
+ fn.onCall(3).resolves('success')
+
+ const res = withRetries(fn, { maxRetries: 4, delay: 100, backoff: 2 })
+
+ // Check the call count after each iteration, ensuring the function is called
+ // after the correct delay between retries.
+ await clock.tickAsync(99)
+ assert.strictEqual(fn.callCount, 1)
+ await clock.tickAsync(1)
+ assert.strictEqual(fn.callCount, 2)
+
+ await clock.tickAsync(199)
+ assert.strictEqual(fn.callCount, 2)
+ await clock.tickAsync(1)
+ assert.strictEqual(fn.callCount, 3)
+
+ await clock.tickAsync(399)
+ assert.strictEqual(fn.callCount, 3)
+ await clock.tickAsync(1)
+ assert.strictEqual(fn.callCount, 4)
+
+ assert.strictEqual(await res, 'success')
+ })
+})
diff --git a/packages/core/src/test/shared/utilities/osUtils.test.ts b/packages/core/src/test/shared/utilities/osUtils.test.ts
index 1654aca8cc8..bd6e4489ac0 100644
--- a/packages/core/src/test/shared/utilities/osUtils.test.ts
+++ b/packages/core/src/test/shared/utilities/osUtils.test.ts
@@ -22,32 +22,32 @@ describe('isNewOsSession', () => {
})
it('unix-like: returns true when expected', async () => {
- const uptimeStub = sandbox.stub()
+ const uptimeMillisStub = sandbox.stub()
const now = sandbox.stub()
- // We started our computer at 2 minutes since epoch (time - pc uptime)
- // and the comptuer has been on for 1 minute. So the OS started 1 minute since epoch.
- now.returns(60_000 + 60_000)
- uptimeStub.returns(1)
+ // We started our computer at 1 minutes since epoch and the comptuer uptime has been 1 minute.
+ // So the OS started at the epoch (time - uptime).
+ now.returns(0) // the epoch time
+ uptimeMillisStub.returns(0) // this session has just started
// On a brand new session the first caller will get true
- assert.strictEqual(await isNewOsSession(now, uptimeStub), true)
+ assert.strictEqual(await isNewOsSession(now, uptimeMillisStub), true)
// Subsequent callers will get false
- assert.strictEqual(await isNewOsSession(now, uptimeStub), false)
-
- // Start a computer session 10 minutes from epoch
- uptimeStub.returns(0)
- now.returns(60_000 * 10)
- assert.strictEqual(await isNewOsSession(now, uptimeStub), true)
- // Anything that is within a 5 second threshold of the last session time, is considered the same session
- now.returns(60_000 * 10 + 5000)
- assert.strictEqual(await isNewOsSession(now, uptimeStub), false)
- now.returns(60_000 * 10 + 5000 + 1)
- assert.strictEqual(await isNewOsSession(now, uptimeStub), true)
-
- // A non-zero uptime
- uptimeStub.returns(5) // The computer has been running for 5 minutes already, so the start time is relative to this.
- now.returns(60_000 * 10 + 5000 + 60_000 * 10) // 5 minutes since last session
- // Nothing changes since the diff between uptime and the last start has not changed
- assert.strictEqual(await isNewOsSession(now, uptimeStub), true)
+ assert.strictEqual(await isNewOsSession(now, uptimeMillisStub), false)
+
+ // 10 minutes later, same session
+ now.returns(1000 * 60 * 10)
+ uptimeMillisStub.returns(1000 * 60 * 10) // This scales proportionately with the current time
+ // This is still the same session, so we get false
+ assert.strictEqual(await isNewOsSession(now, uptimeMillisStub), false)
+
+ // Test the lowerbound of what is considered a new session
+ // Pretend we started a new computer session 5 seconds after the initial session
+ uptimeMillisStub.returns(0)
+ now.returns(5000)
+ // Anything that is within a 5 second threshold of the last session time, is considered the SAME session
+ assert.strictEqual(await isNewOsSession(now, uptimeMillisStub), false)
+ // This is 1 millisecond after the threshold, it is considered a NEW session
+ now.returns(5000 + 1)
+ assert.strictEqual(await isNewOsSession(now, uptimeMillisStub), true)
})
})
diff --git a/packages/core/src/test/testUtil.ts b/packages/core/src/test/testUtil.ts
index 4ee4e9498a1..138b802ba7b 100644
--- a/packages/core/src/test/testUtil.ts
+++ b/packages/core/src/test/testUtil.ts
@@ -75,7 +75,7 @@ export function getWorkspaceFolder(dir: string): vscode.WorkspaceFolder {
* But if the day comes that we need it for web, we should be able to add some agnostic FS methods in here.
*/
export class TestFolder {
- protected constructor(private readonly rootFolder: string) {}
+ protected constructor(public readonly path: string) {}
/** Creates a folder that deletes itself once all tests are done running. */
static async create() {
@@ -118,10 +118,6 @@ export class TestFolder {
pathFrom(relativePath: string): string {
return path.join(this.path, relativePath)
}
-
- get path(): string {
- return path.join(this.rootFolder)
- }
}
/**
From 2859246643eb79a2d81b152eaaf4b057739cb578 Mon Sep 17 00:00:00 2001
From: "Justin M. Keyes"
Date: Thu, 10 Oct 2024 13:11:49 -0700
Subject: [PATCH 31/87] telemetry(messages): maybeShowMinVscodeWarning #5762
## Problem
no metrics for notifications.
## Solution
add `toolkit_showNotification` to `showMessage` and use it in
`maybeShowMinVscodeWarning`.
---
package-lock.json | 2 +-
packages/core/src/shared/extensionStartup.ts | 32 ++++++----
.../core/src/shared/utilities/messages.ts | 59 ++++++++++++++-----
.../test/shared/extensionUtilities.test.ts | 2 +
4 files changed, 66 insertions(+), 29 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index cc42d933cd4..11d464fcc55 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19394,7 +19394,7 @@
},
"engines": {
"npm": "^10.1.0",
- "vscode": "^1.83.0"
+ "vscode": "^1.68.0"
}
},
"packages/core/node_modules/@types/node": {
diff --git a/packages/core/src/shared/extensionStartup.ts b/packages/core/src/shared/extensionStartup.ts
index d10c194d503..f4ed00e9543 100644
--- a/packages/core/src/shared/extensionStartup.ts
+++ b/packages/core/src/shared/extensionStartup.ts
@@ -14,6 +14,8 @@ import { fs } from '../shared/fs/fs'
import { getIdeProperties, getIdeType, isAmazonQ, isCloud9, isCn, productName } from './extensionUtilities'
import * as localizedText from './localizedText'
import { AmazonQPromptSettings, ToolkitPromptSettings } from './settings'
+import { showMessage } from './utilities/messages'
+import { getTelemetryReasonDesc } from './errors'
const localize = nls.loadMessageBundle()
@@ -26,20 +28,24 @@ export async function maybeShowMinVscodeWarning(minVscode: string) {
return
}
const updateButton = `Update ${vscode.env.appName}`
+ const msg = `${productName()} will soon require VS Code ${minVscode} or newer. The currently running version ${vscode.version} will no longer receive updates.`
if (getIdeType() === 'vscode' && semver.lt(vscode.version, minVscode)) {
- void vscode.window
- .showWarningMessage(
- `${productName()} will soon require VS Code ${minVscode} or newer. The currently running version ${vscode.version} will no longer receive updates.`,
- updateButton,
- localizedText.dontShow
- )
- .then(async (resp) => {
- if (resp === updateButton) {
- await vscode.commands.executeCommand('update.checkForUpdate')
- } else if (resp === localizedText.dontShow) {
- void settings.disablePrompt('minIdeVersion')
- }
- })
+ void showMessage(
+ 'warn',
+ msg,
+ [updateButton, localizedText.dontShow],
+ {},
+ {
+ id: 'maybeShowMinVscodeWarning',
+ reasonDesc: getTelemetryReasonDesc(msg),
+ }
+ ).then(async (resp) => {
+ if (resp === updateButton) {
+ await vscode.commands.executeCommand('update.checkForUpdate')
+ } else if (resp === localizedText.dontShow) {
+ void settings.disablePrompt('minIdeVersion')
+ }
+ })
}
}
diff --git a/packages/core/src/shared/utilities/messages.ts b/packages/core/src/shared/utilities/messages.ts
index 7ab473ac18c..1812909321c 100644
--- a/packages/core/src/shared/utilities/messages.ts
+++ b/packages/core/src/shared/utilities/messages.ts
@@ -16,8 +16,9 @@ import { getIcon, codicon } from '../icons'
import globals from '../extensionGlobals'
import { openUrl } from './vsCodeUtils'
import { AmazonQPromptSettings, ToolkitPromptSettings } from '../../shared/settings'
-import { telemetry } from '../telemetry/telemetry'
+import { telemetry, ToolkitShowNotification } from '../telemetry/telemetry'
import { vscodeComponent } from '../vscode/commands2'
+import { getTelemetryReasonDesc } from '../errors'
export const messages = {
editCredentials(icon: boolean) {
@@ -35,21 +36,31 @@ export function makeFailedWriteMessage(filename: string): string {
return message
}
-function showMessageWithItems(
- message: string,
+export function showMessage(
kind: 'info' | 'warn' | 'error' = 'error',
+ message: string,
items: string[] = [],
- useModal: boolean = false
+ options: vscode.MessageOptions & { telemetry?: boolean } = {},
+ metric: Partial = {}
): Thenable {
- switch (kind) {
- case 'info':
- return vscode.window.showInformationMessage(message, { modal: useModal }, ...items)
- case 'warn':
- return vscode.window.showWarningMessage(message, { modal: useModal }, ...items)
- case 'error':
- default:
- return vscode.window.showErrorMessage(message, { modal: useModal }, ...items)
- }
+ return telemetry.toolkit_showNotification.run(async (span) => {
+ span.record({
+ passive: true,
+ id: 'unknown',
+ component: 'editor',
+ ...metric,
+ })
+
+ switch (kind) {
+ case 'info':
+ return vscode.window.showInformationMessage(message, options, ...items)
+ case 'warn':
+ return vscode.window.showWarningMessage(message, options, ...items)
+ case 'error':
+ default:
+ return vscode.window.showErrorMessage(message, options, ...items)
+ }
+ })
}
/**
@@ -75,7 +86,16 @@ export async function showMessageWithUrl(
const uri = typeof url === 'string' ? vscode.Uri.parse(url) : url
const items = [...extraItems, urlItem]
- const p = showMessageWithItems(message, kind, items, useModal)
+ const p = showMessage(
+ kind,
+ message,
+ items,
+ { modal: useModal },
+ {
+ id: 'showMessageWithUrl',
+ reasonDesc: getTelemetryReasonDesc(message),
+ }
+ )
return p.then((selection) => {
if (selection === urlItem) {
void openUrl(uri)
@@ -102,7 +122,16 @@ export async function showViewLogsMessage(
const logsItem = localize('AWS.generic.message.viewLogs', 'View Logs...')
const items = [...extraItems, logsItem]
- const p = showMessageWithItems(message, kind, items)
+ const p = showMessage(
+ kind,
+ message,
+ items,
+ {},
+ {
+ id: 'showViewLogsMessage',
+ reasonDesc: getTelemetryReasonDesc(message),
+ }
+ )
return p.then((selection) => {
if (selection === logsItem) {
globals.logOutputChannel.show(true)
diff --git a/packages/core/src/test/shared/extensionUtilities.test.ts b/packages/core/src/test/shared/extensionUtilities.test.ts
index 255455a54f1..0e29c9f7f06 100644
--- a/packages/core/src/test/shared/extensionUtilities.test.ts
+++ b/packages/core/src/test/shared/extensionUtilities.test.ts
@@ -21,6 +21,7 @@ import globals from '../../shared/extensionGlobals'
import { createQuickStartWebview, maybeShowMinVscodeWarning } from '../../shared/extensionStartup'
import { fs } from '../../shared'
import { getTestWindow } from './vscode/window'
+import { assertTelemetry } from '../testUtil'
describe('extensionUtilities', function () {
it('maybeShowMinVscodeWarning', async () => {
@@ -30,6 +31,7 @@ describe('extensionUtilities', function () {
/will soon require .* 99\.0\.0 or newer. The currently running version .* will no longer receive updates./
const msg = await getTestWindow().waitForMessage(expectedMsg)
msg.close()
+ assertTelemetry('toolkit_showNotification', [])
})
describe('createQuickStartWebview', async function () {
From 600db4292e39870cb7ed94f7795c9d534280cdc7 Mon Sep 17 00:00:00 2001
From: aws-toolkit-automation <>
Date: Thu, 10 Oct 2024 20:14:45 +0000
Subject: [PATCH 32/87] Release 1.29.0
---
package-lock.json | 4 +-
packages/amazonq/.changes/1.29.0.json | 54 +++++++++++++++++++
...-0d324321-bddd-4f93-80c4-80978930f157.json | 4 --
...-3f76aff8-1622-4647-aafe-3210cf0c3b74.json | 4 --
...-5576420e-6db2-4045-a99f-afced496da0f.json | 4 --
...-55ef93ff-1589-41bc-a482-ed09002f1fd9.json | 4 --
...-dd58d9cb-4fb4-4cb3-a9a2-dc5c5462e529.json | 4 --
...-ff32752d-fa0b-4d38-b719-68eb3c9ddca6.json | 4 --
...-4577def6-0191-48db-bec3-480b226285b8.json | 4 --
...-294f0da4-1fb4-43c9-90f2-e066d7351c3c.json | 4 --
...-3ebcda93-ca02-4682-b475-dead26e94900.json | 4 --
...-88ac445a-7a5a-4127-97f7-eaccd41629e6.json | 4 --
...-b02fae9d-cb44-4625-af37-0b119c18ef63.json | 4 --
...-c259464a-0437-47e6-be6c-8df023ad477c.json | 4 --
packages/amazonq/CHANGELOG.md | 15 ++++++
packages/amazonq/package.json | 2 +-
16 files changed, 72 insertions(+), 51 deletions(-)
create mode 100644 packages/amazonq/.changes/1.29.0.json
delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-0d324321-bddd-4f93-80c4-80978930f157.json
delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-3f76aff8-1622-4647-aafe-3210cf0c3b74.json
delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-5576420e-6db2-4045-a99f-afced496da0f.json
delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-55ef93ff-1589-41bc-a482-ed09002f1fd9.json
delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-dd58d9cb-4fb4-4cb3-a9a2-dc5c5462e529.json
delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-ff32752d-fa0b-4d38-b719-68eb3c9ddca6.json
delete mode 100644 packages/amazonq/.changes/next-release/Deprecation-4577def6-0191-48db-bec3-480b226285b8.json
delete mode 100644 packages/amazonq/.changes/next-release/Feature-294f0da4-1fb4-43c9-90f2-e066d7351c3c.json
delete mode 100644 packages/amazonq/.changes/next-release/Feature-3ebcda93-ca02-4682-b475-dead26e94900.json
delete mode 100644 packages/amazonq/.changes/next-release/Feature-88ac445a-7a5a-4127-97f7-eaccd41629e6.json
delete mode 100644 packages/amazonq/.changes/next-release/Feature-b02fae9d-cb44-4625-af37-0b119c18ef63.json
delete mode 100644 packages/amazonq/.changes/next-release/Removal-c259464a-0437-47e6-be6c-8df023ad477c.json
diff --git a/package-lock.json b/package-lock.json
index 11d464fcc55..67e676edc6e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -41,7 +41,7 @@
"prettier": "^3.3.2",
"prettier-plugin-sh": "^0.14.0",
"pretty-quick": "^4.0.0",
- "ts-node": "^10.9.1",
+ "ts-node": "^10.9.2",
"typescript": "^5.0.4",
"webpack": "^5.83.0",
"webpack-cli": "^5.1.4",
@@ -19257,7 +19257,7 @@
},
"packages/amazonq": {
"name": "amazon-q-vscode",
- "version": "1.29.0-SNAPSHOT",
+ "version": "1.29.0",
"license": "Apache-2.0",
"dependencies": {
"aws-core-vscode": "file:../core/"
diff --git a/packages/amazonq/.changes/1.29.0.json b/packages/amazonq/.changes/1.29.0.json
new file mode 100644
index 00000000000..35cc08b1b25
--- /dev/null
+++ b/packages/amazonq/.changes/1.29.0.json
@@ -0,0 +1,54 @@
+{
+ "date": "2024-10-10",
+ "version": "1.29.0",
+ "entries": [
+ {
+ "type": "Bug Fix",
+ "description": "Amazon Q /dev: include telemetry for workspace usage when generating new files"
+ },
+ {
+ "type": "Bug Fix",
+ "description": "Amazon Q extension may fail to start if AWS Toolkit extension fails to start"
+ },
+ {
+ "type": "Bug Fix",
+ "description": "Start language server by default"
+ },
+ {
+ "type": "Bug Fix",
+ "description": "Amazon Q Feature Dev: Add error messages when the upload URL expires"
+ },
+ {
+ "type": "Bug Fix",
+ "description": "Amazon Q (/dev): view diffs of previous /dev iterations"
+ },
+ {
+ "type": "Bug Fix",
+ "description": "Q dev handle no change required"
+ },
+ {
+ "type": "Deprecation",
+ "description": "The next release of this extension will require VS Code 1.83.0 or newer."
+ },
+ {
+ "type": "Feature",
+ "description": "Automatically pause and resume @workspace indexing when OS CPU load is high"
+ },
+ {
+ "type": "Feature",
+ "description": "Add buttons to code blocks to view and accept diffs."
+ },
+ {
+ "type": "Feature",
+ "description": "Inline completion for more json files, and all yaml files"
+ },
+ {
+ "type": "Feature",
+ "description": "Show a one-time warning if new VS Code is required"
+ },
+ {
+ "type": "Removal",
+ "description": "Minimum required VSCode version is now 1.83"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-0d324321-bddd-4f93-80c4-80978930f157.json b/packages/amazonq/.changes/next-release/Bug Fix-0d324321-bddd-4f93-80c4-80978930f157.json
deleted file mode 100644
index 643ff10bb77..00000000000
--- a/packages/amazonq/.changes/next-release/Bug Fix-0d324321-bddd-4f93-80c4-80978930f157.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Bug Fix",
- "description": "Amazon Q /dev: include telemetry for workspace usage when generating new files"
-}
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-3f76aff8-1622-4647-aafe-3210cf0c3b74.json b/packages/amazonq/.changes/next-release/Bug Fix-3f76aff8-1622-4647-aafe-3210cf0c3b74.json
deleted file mode 100644
index e9fab5cf084..00000000000
--- a/packages/amazonq/.changes/next-release/Bug Fix-3f76aff8-1622-4647-aafe-3210cf0c3b74.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Bug Fix",
- "description": "Amazon Q extension may fail to start if AWS Toolkit extension fails to start"
-}
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-5576420e-6db2-4045-a99f-afced496da0f.json b/packages/amazonq/.changes/next-release/Bug Fix-5576420e-6db2-4045-a99f-afced496da0f.json
deleted file mode 100644
index 9a9d4945af8..00000000000
--- a/packages/amazonq/.changes/next-release/Bug Fix-5576420e-6db2-4045-a99f-afced496da0f.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Bug Fix",
- "description": "Start language server by default"
-}
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-55ef93ff-1589-41bc-a482-ed09002f1fd9.json b/packages/amazonq/.changes/next-release/Bug Fix-55ef93ff-1589-41bc-a482-ed09002f1fd9.json
deleted file mode 100644
index 4b0dc2b599d..00000000000
--- a/packages/amazonq/.changes/next-release/Bug Fix-55ef93ff-1589-41bc-a482-ed09002f1fd9.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Bug Fix",
- "description": "Amazon Q Feature Dev: Add error messages when the upload URL expires"
-}
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-dd58d9cb-4fb4-4cb3-a9a2-dc5c5462e529.json b/packages/amazonq/.changes/next-release/Bug Fix-dd58d9cb-4fb4-4cb3-a9a2-dc5c5462e529.json
deleted file mode 100644
index 3883fe075a2..00000000000
--- a/packages/amazonq/.changes/next-release/Bug Fix-dd58d9cb-4fb4-4cb3-a9a2-dc5c5462e529.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Bug Fix",
- "description": "Amazon Q (/dev): view diffs of previous /dev iterations"
-}
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-ff32752d-fa0b-4d38-b719-68eb3c9ddca6.json b/packages/amazonq/.changes/next-release/Bug Fix-ff32752d-fa0b-4d38-b719-68eb3c9ddca6.json
deleted file mode 100644
index afb6952b969..00000000000
--- a/packages/amazonq/.changes/next-release/Bug Fix-ff32752d-fa0b-4d38-b719-68eb3c9ddca6.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Bug Fix",
- "description": "Q dev handle no change required"
-}
diff --git a/packages/amazonq/.changes/next-release/Deprecation-4577def6-0191-48db-bec3-480b226285b8.json b/packages/amazonq/.changes/next-release/Deprecation-4577def6-0191-48db-bec3-480b226285b8.json
deleted file mode 100644
index 78d58a5fbe6..00000000000
--- a/packages/amazonq/.changes/next-release/Deprecation-4577def6-0191-48db-bec3-480b226285b8.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Deprecation",
- "description": "The next release of this extension will require VS Code 1.83.0 or newer."
-}
diff --git a/packages/amazonq/.changes/next-release/Feature-294f0da4-1fb4-43c9-90f2-e066d7351c3c.json b/packages/amazonq/.changes/next-release/Feature-294f0da4-1fb4-43c9-90f2-e066d7351c3c.json
deleted file mode 100644
index bb4f14692b1..00000000000
--- a/packages/amazonq/.changes/next-release/Feature-294f0da4-1fb4-43c9-90f2-e066d7351c3c.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Feature",
- "description": "Automatically pause and resume @workspace indexing when OS CPU load is high"
-}
diff --git a/packages/amazonq/.changes/next-release/Feature-3ebcda93-ca02-4682-b475-dead26e94900.json b/packages/amazonq/.changes/next-release/Feature-3ebcda93-ca02-4682-b475-dead26e94900.json
deleted file mode 100644
index 124bbc0f4bb..00000000000
--- a/packages/amazonq/.changes/next-release/Feature-3ebcda93-ca02-4682-b475-dead26e94900.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Feature",
- "description": "Add buttons to code blocks to view and accept diffs."
-}
diff --git a/packages/amazonq/.changes/next-release/Feature-88ac445a-7a5a-4127-97f7-eaccd41629e6.json b/packages/amazonq/.changes/next-release/Feature-88ac445a-7a5a-4127-97f7-eaccd41629e6.json
deleted file mode 100644
index 029fa9de4ea..00000000000
--- a/packages/amazonq/.changes/next-release/Feature-88ac445a-7a5a-4127-97f7-eaccd41629e6.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Feature",
- "description": "Inline completion for more json files, and all yaml files"
-}
diff --git a/packages/amazonq/.changes/next-release/Feature-b02fae9d-cb44-4625-af37-0b119c18ef63.json b/packages/amazonq/.changes/next-release/Feature-b02fae9d-cb44-4625-af37-0b119c18ef63.json
deleted file mode 100644
index 021933ef072..00000000000
--- a/packages/amazonq/.changes/next-release/Feature-b02fae9d-cb44-4625-af37-0b119c18ef63.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Feature",
- "description": "Show a one-time warning if new VS Code is required"
-}
diff --git a/packages/amazonq/.changes/next-release/Removal-c259464a-0437-47e6-be6c-8df023ad477c.json b/packages/amazonq/.changes/next-release/Removal-c259464a-0437-47e6-be6c-8df023ad477c.json
deleted file mode 100644
index a97df091840..00000000000
--- a/packages/amazonq/.changes/next-release/Removal-c259464a-0437-47e6-be6c-8df023ad477c.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Removal",
- "description": "Minimum required VSCode version is now 1.83"
-}
diff --git a/packages/amazonq/CHANGELOG.md b/packages/amazonq/CHANGELOG.md
index c64783d32bf..fc446c90e9b 100644
--- a/packages/amazonq/CHANGELOG.md
+++ b/packages/amazonq/CHANGELOG.md
@@ -1,3 +1,18 @@
+## 1.29.0 2024-10-10
+
+- **Bug Fix** Amazon Q /dev: include telemetry for workspace usage when generating new files
+- **Bug Fix** Amazon Q extension may fail to start if AWS Toolkit extension fails to start
+- **Bug Fix** Start language server by default
+- **Bug Fix** Amazon Q Feature Dev: Add error messages when the upload URL expires
+- **Bug Fix** Amazon Q (/dev): view diffs of previous /dev iterations
+- **Bug Fix** Q dev handle no change required
+- **Deprecation** The next release of this extension will require VS Code 1.83.0 or newer.
+- **Feature** Automatically pause and resume @workspace indexing when OS CPU load is high
+- **Feature** Add buttons to code blocks to view and accept diffs.
+- **Feature** Inline completion for more json files, and all yaml files
+- **Feature** Show a one-time warning if new VS Code is required
+- **Removal** Minimum required VSCode version is now 1.83
+
## 1.28.0 2024-10-03
- **Bug Fix** Amazon Q /dev: define first folder as a root path for LLM-created files when using workspaces
diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json
index d3c38c991a3..1073663d2b1 100644
--- a/packages/amazonq/package.json
+++ b/packages/amazonq/package.json
@@ -2,7 +2,7 @@
"name": "amazon-q-vscode",
"displayName": "Amazon Q",
"description": "Amazon Q is your generative AI-powered assistant across the software development lifecycle.",
- "version": "1.29.0-SNAPSHOT",
+ "version": "1.29.0",
"extensionKind": [
"workspace"
],
From 2b9c0c0fea3ee32b056537cba25885b715bfc312 Mon Sep 17 00:00:00 2001
From: aws-toolkit-automation <>
Date: Thu, 10 Oct 2024 21:03:15 +0000
Subject: [PATCH 33/87] Update version to snapshot version: 1.30.0-SNAPSHOT
---
package-lock.json | 4 ++--
packages/amazonq/package.json | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 67e676edc6e..440219462cf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -41,7 +41,7 @@
"prettier": "^3.3.2",
"prettier-plugin-sh": "^0.14.0",
"pretty-quick": "^4.0.0",
- "ts-node": "^10.9.2",
+ "ts-node": "^10.9.1",
"typescript": "^5.0.4",
"webpack": "^5.83.0",
"webpack-cli": "^5.1.4",
@@ -19257,7 +19257,7 @@
},
"packages/amazonq": {
"name": "amazon-q-vscode",
- "version": "1.29.0",
+ "version": "1.30.0-SNAPSHOT",
"license": "Apache-2.0",
"dependencies": {
"aws-core-vscode": "file:../core/"
diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json
index 1073663d2b1..805028fbdb2 100644
--- a/packages/amazonq/package.json
+++ b/packages/amazonq/package.json
@@ -2,7 +2,7 @@
"name": "amazon-q-vscode",
"displayName": "Amazon Q",
"description": "Amazon Q is your generative AI-powered assistant across the software development lifecycle.",
- "version": "1.29.0",
+ "version": "1.30.0-SNAPSHOT",
"extensionKind": [
"workspace"
],
From 16c587f66815cc08bfe18fb926af3e8d70b46d82 Mon Sep 17 00:00:00 2001
From: aws-toolkit-automation <>
Date: Thu, 10 Oct 2024 20:14:45 +0000
Subject: [PATCH 34/87] Release 3.28.0
---
package-lock.json | 4 +--
packages/toolkit/.changes/3.28.0.json | 26 +++++++++++++++++++
...-91df9fbf-ea4b-4087-b43f-77f35253c525.json | 4 ---
...-d8111f1d-c5f8-48ee-9b25-6d64a98bc3c2.json | 4 ---
...-2df766d2-cbec-4fdc-bb69-80f90d9d15ef.json | 4 ---
...-7dfff66e-77a0-478d-8b74-6f3990b85a16.json | 4 ---
...-cf2f67d7-fd40-4834-960c-bbd57508a7f4.json | 4 ---
packages/toolkit/CHANGELOG.md | 8 ++++++
packages/toolkit/package.json | 2 +-
9 files changed, 37 insertions(+), 23 deletions(-)
create mode 100644 packages/toolkit/.changes/3.28.0.json
delete mode 100644 packages/toolkit/.changes/next-release/Breaking Change-91df9fbf-ea4b-4087-b43f-77f35253c525.json
delete mode 100644 packages/toolkit/.changes/next-release/Bug Fix-d8111f1d-c5f8-48ee-9b25-6d64a98bc3c2.json
delete mode 100644 packages/toolkit/.changes/next-release/Deprecation-2df766d2-cbec-4fdc-bb69-80f90d9d15ef.json
delete mode 100644 packages/toolkit/.changes/next-release/Feature-7dfff66e-77a0-478d-8b74-6f3990b85a16.json
delete mode 100644 packages/toolkit/.changes/next-release/Removal-cf2f67d7-fd40-4834-960c-bbd57508a7f4.json
diff --git a/package-lock.json b/package-lock.json
index 440219462cf..23a1c728c6e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -41,7 +41,7 @@
"prettier": "^3.3.2",
"prettier-plugin-sh": "^0.14.0",
"pretty-quick": "^4.0.0",
- "ts-node": "^10.9.1",
+ "ts-node": "^10.9.2",
"typescript": "^5.0.4",
"webpack": "^5.83.0",
"webpack-cli": "^5.1.4",
@@ -19418,7 +19418,7 @@
},
"packages/toolkit": {
"name": "aws-toolkit-vscode",
- "version": "3.28.0-SNAPSHOT",
+ "version": "3.28.0",
"license": "Apache-2.0",
"dependencies": {
"aws-core-vscode": "file:../core/"
diff --git a/packages/toolkit/.changes/3.28.0.json b/packages/toolkit/.changes/3.28.0.json
new file mode 100644
index 00000000000..524679e9c3e
--- /dev/null
+++ b/packages/toolkit/.changes/3.28.0.json
@@ -0,0 +1,26 @@
+{
+ "date": "2024-10-10",
+ "version": "3.28.0",
+ "entries": [
+ {
+ "type": "Breaking Change",
+ "description": "Bumping VS Code minimum version to 1.83.0"
+ },
+ {
+ "type": "Bug Fix",
+ "description": "update animate graph for infraComposer in toolkit README"
+ },
+ {
+ "type": "Deprecation",
+ "description": "The next release of this extension will require VS Code 1.83.0 or newer."
+ },
+ {
+ "type": "Feature",
+ "description": "Show a one-time warning if new VS Code is required"
+ },
+ {
+ "type": "Removal",
+ "description": "Minimum required VSCode version is now 1.83"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/toolkit/.changes/next-release/Breaking Change-91df9fbf-ea4b-4087-b43f-77f35253c525.json b/packages/toolkit/.changes/next-release/Breaking Change-91df9fbf-ea4b-4087-b43f-77f35253c525.json
deleted file mode 100644
index 205db3ae630..00000000000
--- a/packages/toolkit/.changes/next-release/Breaking Change-91df9fbf-ea4b-4087-b43f-77f35253c525.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Breaking Change",
- "description": "Bumping VS Code minimum version to 1.83.0"
-}
diff --git a/packages/toolkit/.changes/next-release/Bug Fix-d8111f1d-c5f8-48ee-9b25-6d64a98bc3c2.json b/packages/toolkit/.changes/next-release/Bug Fix-d8111f1d-c5f8-48ee-9b25-6d64a98bc3c2.json
deleted file mode 100644
index 9c2256c03d6..00000000000
--- a/packages/toolkit/.changes/next-release/Bug Fix-d8111f1d-c5f8-48ee-9b25-6d64a98bc3c2.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Bug Fix",
- "description": "update animate graph for infraComposer in toolkit README"
-}
diff --git a/packages/toolkit/.changes/next-release/Deprecation-2df766d2-cbec-4fdc-bb69-80f90d9d15ef.json b/packages/toolkit/.changes/next-release/Deprecation-2df766d2-cbec-4fdc-bb69-80f90d9d15ef.json
deleted file mode 100644
index 78d58a5fbe6..00000000000
--- a/packages/toolkit/.changes/next-release/Deprecation-2df766d2-cbec-4fdc-bb69-80f90d9d15ef.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Deprecation",
- "description": "The next release of this extension will require VS Code 1.83.0 or newer."
-}
diff --git a/packages/toolkit/.changes/next-release/Feature-7dfff66e-77a0-478d-8b74-6f3990b85a16.json b/packages/toolkit/.changes/next-release/Feature-7dfff66e-77a0-478d-8b74-6f3990b85a16.json
deleted file mode 100644
index 021933ef072..00000000000
--- a/packages/toolkit/.changes/next-release/Feature-7dfff66e-77a0-478d-8b74-6f3990b85a16.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Feature",
- "description": "Show a one-time warning if new VS Code is required"
-}
diff --git a/packages/toolkit/.changes/next-release/Removal-cf2f67d7-fd40-4834-960c-bbd57508a7f4.json b/packages/toolkit/.changes/next-release/Removal-cf2f67d7-fd40-4834-960c-bbd57508a7f4.json
deleted file mode 100644
index a97df091840..00000000000
--- a/packages/toolkit/.changes/next-release/Removal-cf2f67d7-fd40-4834-960c-bbd57508a7f4.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Removal",
- "description": "Minimum required VSCode version is now 1.83"
-}
diff --git a/packages/toolkit/CHANGELOG.md b/packages/toolkit/CHANGELOG.md
index 22e2140e14f..4e4b1a64f02 100644
--- a/packages/toolkit/CHANGELOG.md
+++ b/packages/toolkit/CHANGELOG.md
@@ -1,3 +1,11 @@
+## 3.28.0 2024-10-10
+
+- **Breaking Change** Bumping VS Code minimum version to 1.83.0
+- **Bug Fix** update animate graph for infraComposer in toolkit README
+- **Deprecation** The next release of this extension will require VS Code 1.83.0 or newer.
+- **Feature** Show a one-time warning if new VS Code is required
+- **Removal** Minimum required VSCode version is now 1.83
+
## 3.27.0 2024-10-03
- **Bug Fix** rename Application Composer to Infrastructure Composer
diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json
index 43acf33053c..0d3e933c502 100644
--- a/packages/toolkit/package.json
+++ b/packages/toolkit/package.json
@@ -2,7 +2,7 @@
"name": "aws-toolkit-vscode",
"displayName": "AWS Toolkit",
"description": "Including CodeCatalyst, Infrastructure Composer, and support for Lambda, S3, CloudWatch Logs, CloudFormation, and many other services.",
- "version": "3.28.0-SNAPSHOT",
+ "version": "3.28.0",
"extensionKind": [
"workspace"
],
From 36c72965ffc1d82cf07520e23ba185daa353a794 Mon Sep 17 00:00:00 2001
From: aws-toolkit-automation <>
Date: Thu, 10 Oct 2024 20:48:06 +0000
Subject: [PATCH 35/87] Update version to snapshot version: 3.29.0-SNAPSHOT
---
package-lock.json | 4 ++--
packages/toolkit/package.json | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 23a1c728c6e..d39a924c338 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -41,7 +41,7 @@
"prettier": "^3.3.2",
"prettier-plugin-sh": "^0.14.0",
"pretty-quick": "^4.0.0",
- "ts-node": "^10.9.2",
+ "ts-node": "^10.9.1",
"typescript": "^5.0.4",
"webpack": "^5.83.0",
"webpack-cli": "^5.1.4",
@@ -19418,7 +19418,7 @@
},
"packages/toolkit": {
"name": "aws-toolkit-vscode",
- "version": "3.28.0",
+ "version": "3.29.0-SNAPSHOT",
"license": "Apache-2.0",
"dependencies": {
"aws-core-vscode": "file:../core/"
diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json
index 0d3e933c502..416a0eddfc0 100644
--- a/packages/toolkit/package.json
+++ b/packages/toolkit/package.json
@@ -2,7 +2,7 @@
"name": "aws-toolkit-vscode",
"displayName": "AWS Toolkit",
"description": "Including CodeCatalyst, Infrastructure Composer, and support for Lambda, S3, CloudWatch Logs, CloudFormation, and many other services.",
- "version": "3.28.0",
+ "version": "3.29.0-SNAPSHOT",
"extensionKind": [
"workspace"
],
From af64f3609e2d008ce48ff07d2c9916eb12a0c603 Mon Sep 17 00:00:00 2001
From: "Justin M. Keyes"
Date: Thu, 10 Oct 2024 14:52:49 -0700
Subject: [PATCH 36/87] feat(dev-mode): always log to aws.dev.logfile at
"debug" log-level #5770
Problem:
When debugging the extension, vscode tends to ignore the log-level that
was set from the `AWS Toolkit Logs` UI.
Solution:
If the dev-mode `aws.dev.logfile` path is configured, log to it at
"debug" log-level regardless of the (initial)
`vscode.LogOutputChannel.logLevel`.
---
CONTRIBUTING.md | 18 ++++++++++--------
packages/core/src/shared/logger/activation.ts | 18 +++++++++++-------
packages/core/src/shared/logger/logger.ts | 7 +++++++
.../src/test/shared/logger/activation.test.ts | 2 +-
4 files changed, 29 insertions(+), 16 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c0e946609f9..311aebf7e65 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -345,14 +345,16 @@ The `aws.dev.forceDevMode` setting enables or disables Toolkit "dev mode". Witho
- Example: `getLogger().error('topic: widget failed: %O', { foo: 'bar', baz: 42 })`
- Log messages are written to the extension Output channel, which you can view in vscode by visiting the "Output" panel and selecting `AWS Toolkit Logs` or `Amazon Q Logs`.
- Use the `aws.dev.logfile` setting to set the logfile path to a fixed location, so you can follow
- and filter logs using shell tools like `tail` and `grep`. For example in settings.json,
- ```
- "aws.dev.logfile": "~/awstoolkit.log",
- ```
- then you can tail the logfile in your terminal:
- ```
- tail -F ~/awstoolkit.log
- ```
+ and filter logs using shell tools like `tail` and `grep`.
+ - Note: this always logs at **debug log-level** (though you can temporarily override that from the `AWS Toolkit Logs` UI).
+ - Example `settings.json`:
+ ```
+ "aws.dev.logfile": "~/awstoolkit.log",
+ ```
+ then you can tail the logfile in your terminal:
+ ```
+ tail -F ~/awstoolkit.log
+ ```
- Use the `AWS (Developer): Watch Logs` command to watch and filter Toolkit logs (including
telemetry) in VSCode.
- Only available if you enabled "dev mode" (`aws.dev.forceDevMode` setting, see above).
diff --git a/packages/core/src/shared/logger/activation.ts b/packages/core/src/shared/logger/activation.ts
index a7c12e95513..29d8f8f6fd9 100644
--- a/packages/core/src/shared/logger/activation.ts
+++ b/packages/core/src/shared/logger/activation.ts
@@ -35,7 +35,7 @@ export async function activate(
const mainLogger = makeLogger({
logLevel: chanLogLevel,
- logPaths: logUri ? [logUri] : undefined,
+ logFile: logUri,
outputChannels: [logChannel],
useConsoleLog: isWeb(),
})
@@ -43,6 +43,7 @@ export async function activate(
const newLogLevel = fromVscodeLogLevel(logLevel)
mainLogger.setLogLevel(newLogLevel) // Also logs a message.
})
+ mainLogger.setLogLevel('debug') // HACK: set to "debug" when debugging the extension.
setLogger(mainLogger)
@@ -56,7 +57,7 @@ export async function activate(
'debugConsole'
)
- getLogger().info('Log level: %s%s', chanLogLevel, logUri ? `, file: ${logUri.fsPath}` : '')
+ getLogger().info('Log level: %s%s', chanLogLevel, logUri ? `, file (always "debug" level): ${logUri.fsPath}` : '')
getLogger().debug('User agent: %s', getUserAgent({ includePlatform: true, includeClientId: true }))
if (devLogfile && typeof devLogfile !== 'string') {
getLogger().error('invalid aws.dev.logfile setting')
@@ -69,20 +70,23 @@ export async function activate(
/**
* Creates a logger off of specified params
* @param opts.logLevel Log messages at or above this level
- * @param opts.logPaths Array of paths to output log entries to
+ * @param opts.logFile See {@link Logger.logFile}
* @param opts.outputChannels Array of output channels to log entries to
* @param opts.useConsoleLog If true, outputs log entries to the nodejs or browser devtools console.
*/
export function makeLogger(opts: {
logLevel: LogLevel
- logPaths?: vscode.Uri[]
+ logFile?: vscode.Uri
outputChannels?: vscode.OutputChannel[]
useConsoleLog?: boolean
}): Logger {
const logger = new ToolkitLogger(opts.logLevel)
- // debug console can show ANSI colors, output channels can not
- for (const logPath of opts.logPaths ?? []) {
- logger.logToFile(logPath)
+ if (opts.logFile) {
+ logger.logToFile(opts.logFile)
+ logger.logFile = opts.logFile
+ // XXX: `vscode.LogOutputChannel` does not support programmatically setting the log-level,
+ // so this has no effect there. But this at least enables sinking to `SharedFileTransport`.
+ logger.setLogLevel('debug')
}
for (const outputChannel of opts.outputChannels ?? []) {
logger.logToOutputChannel(outputChannel)
diff --git a/packages/core/src/shared/logger/logger.ts b/packages/core/src/shared/logger/logger.ts
index a1a2dd6a225..3c602c2db35 100644
--- a/packages/core/src/shared/logger/logger.ts
+++ b/packages/core/src/shared/logger/logger.ts
@@ -12,6 +12,11 @@ const toolkitLoggers: {
} = { main: undefined, debugConsole: undefined }
export interface Logger {
+ /**
+ * Developer-only: Optional log file, which gets all log messages (regardless of the configured
+ * log-level).
+ */
+ logFile?: vscode.Uri
debug(message: string | Error, ...meta: any[]): number
verbose(message: string | Error, ...meta: any[]): number
info(message: string | Error, ...meta: any[]): number
@@ -27,6 +32,8 @@ export interface Logger {
}
export abstract class BaseLogger implements Logger {
+ logFile?: vscode.Uri
+
debug(message: string | Error, ...meta: any[]): number {
return this.sendToLog('debug', message, ...meta)
}
diff --git a/packages/core/src/test/shared/logger/activation.test.ts b/packages/core/src/test/shared/logger/activation.test.ts
index df78041ca83..a9167c70ee9 100644
--- a/packages/core/src/test/shared/logger/activation.test.ts
+++ b/packages/core/src/test/shared/logger/activation.test.ts
@@ -18,7 +18,7 @@ describe('makeLogger', function () {
before(async function () {
tempFolder = await makeTemporaryToolkitFolder()
const logPath = vscode.Uri.joinPath(vscode.Uri.file(tempFolder), 'log.txt')
- testLogger = makeLogger({ logLevel: 'debug', logPaths: [logPath] })
+ testLogger = makeLogger({ logLevel: 'debug', logFile: logPath })
})
after(async function () {
From 78f3968e95e7c623bf6c2839f3076056be155ff2 Mon Sep 17 00:00:00 2001
From: "Justin M. Keyes"
Date: Fri, 11 Oct 2024 08:44:29 -0700
Subject: [PATCH 37/87] build: revert "build: temporarily relax min vscode
version" #5771
This reverts commit 1137ac8d7e0f3443ea16c5b7bbe7a4c9326853a1.
---
packages/amazonq/package.json | 2 +-
packages/core/package.json | 2 +-
packages/toolkit/package.json | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json
index 805028fbdb2..38ae92fee85 100644
--- a/packages/amazonq/package.json
+++ b/packages/amazonq/package.json
@@ -1050,6 +1050,6 @@
},
"engines": {
"npm": "^10.1.0",
- "vscode": "^1.68.0"
+ "vscode": "^1.83.0"
}
}
diff --git a/packages/core/package.json b/packages/core/package.json
index 74d0eb2011d..c549bbd9e53 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -5,7 +5,7 @@
"license": "Apache-2.0",
"engines": {
"npm": "^10.1.0",
- "vscode": "^1.68.0"
+ "vscode": "^1.83.0"
},
"exports": {
".": "./dist/src/extension.js",
diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json
index 416a0eddfc0..84a0b235940 100644
--- a/packages/toolkit/package.json
+++ b/packages/toolkit/package.json
@@ -53,7 +53,7 @@
"browser": "./dist/src/extensionWeb",
"engines": {
"npm": "^10.1.0",
- "vscode": "^1.68.0"
+ "vscode": "^1.83.0"
},
"scripts": {
"vscode:prepublish": "npm run clean && npm run buildScripts && webpack --mode production",
From fe0ec31820f22e80329d5828ce136d0f7ccf20a0 Mon Sep 17 00:00:00 2001
From: Hweinstock <42325418+Hweinstock@users.noreply.github.com>
Date: Fri, 11 Oct 2024 12:04:09 -0400
Subject: [PATCH 38/87] fix(tests): use node:fs when globals are not
initialized #5772
## Problem
In
https://github.com/aws/aws-toolkit-vscode/blob/16aa3684f479566bfcf6a9e33f88e70039831e9a/packages/core/src/test/globalSetup.test.ts#L48,
we use our fs.fs module which accesses globals.isWeb, but globals are not initialized yet.
```
Exception has occurred: Error: ToolkitGlobals accessed before initialize()
at Object.get (/Volumes/workplace/aws-toolkit-vscode/packages/core/src/shared/extensionGlobals.ts:109:23)
at FileSystem.get isWeb [as isWeb] (/Volumes/workplace/aws-toolkit-vscode/packages/core/src/shared/fs/fs.ts:689:24)
at FileSystem.mkdir (/Volumes/workplace/aws-toolkit-vscode/packages/core/src/shared/fs/fs.ts:94:63)
at Runner. (/Volumes/workplace/aws-toolkit-vscode/packages/core/src/test/globalSetup.test.ts:48:18)
```
To reproduce, go into master, run any test file individually with
"Extension Tests (current file) (amazonq)".
## Solution
Use node's fs when setting up tests since we must wait for global
context to be initialized to use our fs.
---
packages/core/src/test/globalSetup.test.ts | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/packages/core/src/test/globalSetup.test.ts b/packages/core/src/test/globalSetup.test.ts
index 9f1bbd9d154..c7bc947c482 100644
--- a/packages/core/src/test/globalSetup.test.ts
+++ b/packages/core/src/test/globalSetup.test.ts
@@ -26,9 +26,9 @@ import { GlobalState } from '../shared/globalState'
import { FeatureConfigProvider } from '../shared/featureConfig'
import { mockFeatureConfigsData } from './fake/mockFeatureConfigData'
import { fs } from '../shared'
+import { promises as nodefs } from 'fs' //eslint-disable-line no-restricted-imports
disableAwsSdkWarning()
-
const testReportDir = join(__dirname, '../../../../../.test-reports') // Root project, not subproject
const testLogOutput = join(testReportDir, 'testLog.log')
const globalSandbox = sinon.createSandbox()
@@ -42,10 +42,9 @@ let openExternalStub: sinon.SinonStub
Date: Fri, 11 Oct 2024 13:38:10 -0700
Subject: [PATCH 39/87] build(deps): bump the npm_and_yarn group across 1
directory with 2 updates (#5752)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps the npm_and_yarn group with 2 updates in the / directory:
[cookie](https://github.com/jshttp/cookie) and
[express](https://github.com/expressjs/express).
Updates `cookie` from 0.6.0 to 0.7.1
Release notes
Sourced from cookie's
releases.
0.7.1
Fixed
- Allow leading dot for domain (#174)
- Although not permitted in the spec, some users expect this to work
and user agents ignore the leading dot according to spec
- Add fast path for
serialize without options, use
obj.hasOwnProperty when parsing (#172)
https://github.com/jshttp/cookie/compare/v0.7.0...v0.7.1
0.7.0
https://github.com/jshttp/cookie/compare/v0.6.0...v0.7.0
Commits
Maintainer changes
This version was pushed to npm by blakeembrey, a new
releaser for cookie since your current version.
Updates `express` from 4.21.0 to 4.21.1
Release notes
Sourced from express's
releases.
4.21.1
What's Changed
Full Changelog: https://github.com/expressjs/express/compare/4.21.0...4.21.1
Changelog
Sourced from express's
changelog.
4.21.1 / 2024-10-08
Commits
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore ` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore ` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore ` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/aws/aws-toolkit-vscode/network/alerts).
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index d39a924c338..bd06c2c49e5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9790,9 +9790,10 @@
"license": "MIT"
},
"node_modules/cookie": {
- "version": "0.6.0",
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -11361,16 +11362,17 @@
}
},
"node_modules/express": {
- "version": "4.21.0",
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
+ "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
- "cookie": "0.6.0",
+ "cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
From b3dbfd1e8e9f88714dd92491384f9e7de72489e2 Mon Sep 17 00:00:00 2001
From: Josh Pinkney <103940141+jpinkney-aws@users.noreply.github.com>
Date: Fri, 11 Oct 2024 16:46:46 -0400
Subject: [PATCH 40/87] docs(amazonq): sequence diagram for performance
telemetry #5743
## Problem
It's hard to know what certain performance metrics are tracking
## Solution
Add documentation
---
docs/telemetry-perf.md | 260 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 260 insertions(+)
create mode 100644 docs/telemetry-perf.md
diff --git a/docs/telemetry-perf.md b/docs/telemetry-perf.md
new file mode 100644
index 00000000000..74334183ca4
--- /dev/null
+++ b/docs/telemetry-perf.md
@@ -0,0 +1,260 @@
+# Telemetry Performance Metrics
+
+Visual representations of performance telemetry metrics
+
+## Amazon Q Inline
+
+### codewhispererFirstCompletionLatency
+
+How long it took to receive the first suggestion after we started calling the getRecommendations API
+
+```mermaid
+ sequenceDiagram
+ participant User
+ participant invoke as Inline invoked
+ participant rService as Recommendation Service
+ participant rHandler as Recommendation Handler
+ participant backend as CWSPR backend
+ participant sdk as Create CWSPR SDK
+ participant token as Toolkit auth
+
+
+ User->>invoke: Finished typing
+ invoke->>rService: calls
+ rService->>rHandler: calls
+ rHandler->>sdk: calls
+
+ sdk->>token: Start getting bearer token
+ token->>sdk: Finished getting bearer token
+
+ sdk->>rHandler: Return client
+ note over rHandler, backend: codewhispererFirstCompletionLatency
+ rect rgb(230, 230, 230, 0.5)
+ loop Get paginated recommendations
+ rHandler->>backend: calls
+ end
+ backend->>rHandler: first response received
+ end
+ rHandler->>User: show results
+ backend->>rHandler: all the other responses
+ rHandler->>User: add to already shown results
+```
+
+### codewhispererEndToEndLatency
+
+How long it took from when we started calling the getRecommendations API to when the first suggestion was shown
+
+```mermaid
+ sequenceDiagram
+ participant User
+ participant invoke as Inline invoked
+ participant rService as Recommendation Service
+ participant rHandler as Recommendation Handler
+ participant backend as CWSPR backend
+ participant sdk as Create CWSPR SDK
+ participant token as Toolkit auth
+
+ User->>invoke: Finished typing
+ invoke->>rService: calls
+ rService->>rHandler: calls
+ rHandler->>sdk: calls
+
+ sdk->>token: Start getting bearer token
+ token->>sdk: Finished getting bearer token
+
+ sdk->>rHandler: Return client
+ note over User, backend: codewhispererEndToEndLatency
+ rect rgb(230, 230, 230, 0.5)
+ loop Get paginated recommendations
+ rHandler->>backend: calls
+ end
+ backend->>rHandler: first response received
+ rHandler->>User: show results
+ end
+
+ backend->>rHandler: all the other responses
+ rHandler->>User: add to already shown results
+```
+
+### codewhispererAllCompletionsLatency
+
+How long it took to complete all paginated calls
+
+```mermaid
+ sequenceDiagram
+ participant User
+ participant invoke as Inline invoked
+ participant rService as Recommendation Service
+ participant rHandler as Recommendation Handler
+ participant backend as CWSPR backend
+ participant sdk as Create CWSPR SDK
+ participant token as Toolkit auth
+
+
+ User->>invoke: Finished typing
+ invoke->>rService: calls
+ rService->>rHandler: calls
+ rHandler->>sdk: calls
+
+ sdk->>token: Start getting bearer token
+ token->>sdk: Finished getting bearer token
+
+ sdk->>rHandler: Return client
+ note over User, backend: codewhispererAllCompletionsLatency
+ rect rgb(230, 230, 230, 0.5)
+ loop Get paginated recommendations
+ rHandler->>backend: calls
+ end
+ backend->>rHandler: first response received
+ rHandler->>User: show results
+ backend->>rHandler: all the other responses
+ end
+
+
+ rHandler->>User: add to already shown results
+```
+
+### codewhispererPostprocessingLatency
+
+How long it took to display the first suggestion after it received the first response from the API
+
+```mermaid
+ sequenceDiagram
+ participant User
+ participant invoke as Inline invoked
+ participant rService as Recommendation Service
+ participant rHandler as Recommendation Handler
+ participant backend as CWSPR backend
+ participant sdk as Create CWSPR SDK
+ participant token as Toolkit auth
+
+
+ User->>invoke: Finished typing
+ invoke->>rService: calls
+ rService->>rHandler: calls
+ rHandler->>sdk: calls
+
+ sdk->>token: Start getting bearer token
+ token->>sdk: Finished getting bearer token
+
+ sdk->>rHandler: Return client
+ loop Get paginated recommendations
+ rHandler->>backend: calls
+ end
+ note over User, backend: codewhispererPostprocessingLatency
+ rect rgb(230, 230, 230, 0.5)
+ backend->>rHandler: first response received
+ rHandler->>User: show results
+ end
+
+ backend->>rHandler: all the other responses
+ rHandler->>User: add to already shown results
+```
+
+### codewhispererCredentialFetchingLatency
+
+How long it took to get the bearer token
+
+```mermaid
+ sequenceDiagram
+ participant User
+ participant invoke as Inline invoked
+ participant rService as Recommendation Service
+ participant rHandler as Recommendation Handler
+ participant backend as CWSPR backend
+ participant sdk as Create CWSPR SDK
+ participant token as Toolkit auth
+
+ User->>invoke: Finished typing
+ invoke->>rService: calls
+ rService->>rHandler: calls
+ rHandler->>sdk: calls
+
+ note over sdk, token: codewhispererCredentialFetchingLatency
+ rect rgb(230, 230, 230, 0.5)
+ sdk->>token: Start getting bearer token
+ token->>sdk: Finished getting bearer token
+ end
+ sdk->>rHandler: Return client
+ loop Get paginated recommendations
+ rHandler->>backend: calls
+ end
+
+ backend->>rHandler: first response received
+ rHandler->>User: show results
+
+ backend->>rHandler: all the other responses
+ rHandler->>User: add to already shown results
+```
+
+### codewhispererPreprocessingLatency
+
+How long it took to create the client and get ready to start sending getRecommendation API calls
+
+```mermaid
+ sequenceDiagram
+ participant User
+ participant invoke as Inline invoked
+ participant rService as Recommendation Service
+ participant rHandler as Recommendation Handler
+ participant backend as CWSPR backend
+ participant sdk as Create CWSPR SDK
+ participant token as Toolkit auth
+
+ User->>invoke: Finished typing
+ invoke->>rService: calls
+ rService->>rHandler: calls
+ rHandler->>sdk: calls
+
+ note over rHandler, token: codewhispererPreprocessingLatency
+ rect rgb(230, 230, 230, 0.5)
+ sdk->>token: Start getting bearer token
+ token->>sdk: Finished getting bearer token
+ sdk->>rHandler: Return client
+ end
+ loop Get paginated recommendations
+ rHandler->>backend: calls
+ end
+
+ backend->>rHandler: first response received
+ rHandler->>User: show results
+
+ backend->>rHandler: all the other responses
+ rHandler->>User: add to already shown results
+```
+
+### codewhisperer_perceivedLatency duration
+
+How long it took from when the user stopped pressing a key to when they were shown a response
+
+```mermaid
+ sequenceDiagram
+ participant User
+ participant invoke as Inline invoked
+ participant rService as Recommendation Service
+ participant rHandler as Recommendation Handler
+ participant backend as CWSPR backend
+ participant sdk as Create CWSPR SDK
+ participant token as Toolkit auth
+
+ User->>invoke: Finished typing
+ note over User, token: codewhisperer_perceivedLatency duration
+ rect rgb(230, 230, 230, 0.5)
+ invoke->>rService: calls
+ rService->>rHandler: calls
+ rHandler->>sdk: calls
+ sdk->>token: Start getting bearer token
+ token->>sdk: Finished getting bearer token
+ sdk->>rHandler: Return client
+
+ loop Get paginated recommendations
+ rHandler->>backend: calls
+ end
+
+ backend->>rHandler: first response received
+ rHandler->>User: show results
+
+ backend->>rHandler: all the other responses
+ rHandler->>User: add to already shown results
+ end
+```
From 62d6c8804e973a02d3475489ffb6eccb9f8ec52b Mon Sep 17 00:00:00 2001
From: Hweinstock <42325418+Hweinstock@users.noreply.github.com>
Date: Mon, 14 Oct 2024 15:18:32 -0400
Subject: [PATCH 41/87] refactor(tests): move performance tests to
testInteg/perf/ #5735
## Problem
Performance test involve running the same code 10 times, and often its
demanding code. This should live with integ rather than unit tests to
avoid slow down.
## Solution
- Move
https://github.com/aws/aws-toolkit-vscode/blob/master/packages/core/src/test/amazonqFeatureDev/prepareRepoData.test.ts
into integ folder.
- Move
https://github.com/aws/aws-toolkit-vscode/blob/master/packages/core/src/test/codewhisperer/commands/startSecurityScan.test.ts
into integ folder.
---
.../{commands => }/startSecurityScan.test.ts | 218 ++----------------
.../core/src/test/codewhisperer/testUtil.ts | 107 ++++++++-
.../testInteg/{ => perf}/buildIndex.test.ts | 10 +-
.../src/testInteg/perf/collectFiles.test.ts | 62 +++++
.../perf}/prepareRepoData.test.ts | 2 +-
.../testInteg/perf/startSecurityScan.test.ts | 96 ++++++++
.../{ => perf}/tryInstallLsp.test.ts | 8 +-
.../src/testInteg/{ => perf}/zipcode.test.ts | 12 +-
.../shared/utilities/workspaceUtils.test.ts | 52 -----
9 files changed, 296 insertions(+), 271 deletions(-)
rename packages/core/src/test/codewhisperer/{commands => }/startSecurityScan.test.ts (65%)
rename packages/core/src/testInteg/{ => perf}/buildIndex.test.ts (87%)
create mode 100644 packages/core/src/testInteg/perf/collectFiles.test.ts
rename packages/core/src/{test/amazonqFeatureDev => testInteg/perf}/prepareRepoData.test.ts (97%)
create mode 100644 packages/core/src/testInteg/perf/startSecurityScan.test.ts
rename packages/core/src/testInteg/{ => perf}/tryInstallLsp.test.ts (94%)
rename packages/core/src/testInteg/{ => perf}/zipcode.test.ts (87%)
diff --git a/packages/core/src/test/codewhisperer/commands/startSecurityScan.test.ts b/packages/core/src/test/codewhisperer/startSecurityScan.test.ts
similarity index 65%
rename from packages/core/src/test/codewhisperer/commands/startSecurityScan.test.ts
rename to packages/core/src/test/codewhisperer/startSecurityScan.test.ts
index f3efda3e040..fadd1063aa8 100644
--- a/packages/core/src/test/codewhisperer/commands/startSecurityScan.test.ts
+++ b/packages/core/src/test/codewhisperer/startSecurityScan.test.ts
@@ -7,130 +7,28 @@ import assert from 'assert'
import * as vscode from 'vscode'
import * as sinon from 'sinon'
import * as semver from 'semver'
-import { DefaultCodeWhispererClient } from '../../../codewhisperer/client/codewhisperer'
-import * as startSecurityScan from '../../../codewhisperer/commands/startSecurityScan'
-import { SecurityPanelViewProvider } from '../../../codewhisperer/views/securityPanelViewProvider'
-import { FakeExtensionContext } from '../../fakeExtensionContext'
-import * as diagnosticsProvider from '../../../codewhisperer/service/diagnosticsProvider'
-import { getTestWorkspaceFolder } from '../../../testInteg/integrationTestsUtilities'
+import * as startSecurityScan from '../../codewhisperer/commands/startSecurityScan'
+import { SecurityPanelViewProvider } from '../../codewhisperer/views/securityPanelViewProvider'
+import { FakeExtensionContext } from '../fakeExtensionContext'
+import * as diagnosticsProvider from '../../codewhisperer/service/diagnosticsProvider'
+import { getTestWorkspaceFolder } from '../../testInteg/integrationTestsUtilities'
import { join } from 'path'
-import {
- assertTelemetry,
- closeAllEditors,
- createTestWorkspaceFolder,
- getFetchStubWithResponse,
- toFile,
-} from '../../testUtil'
-import { stub } from '../../utilities/stubber'
-import { AWSError, HttpResponse } from 'aws-sdk'
-import { getTestWindow } from '../../shared/vscode/window'
-import { SeverityLevel } from '../../shared/vscode/message'
-import { cancel } from '../../../shared/localizedText'
+import { assertTelemetry, closeAllEditors, getFetchStubWithResponse } from '../testUtil'
+import { AWSError } from 'aws-sdk'
+import { getTestWindow } from '../shared/vscode/window'
+import { SeverityLevel } from '../shared/vscode/message'
+import { cancel } from '../../shared/localizedText'
import {
showScannedFilesMessage,
stopScanMessage,
CodeAnalysisScope,
projectScansLimitReached,
-} from '../../../codewhisperer/models/constants'
-import * as model from '../../../codewhisperer/models/model'
-import { CodewhispererSecurityScan } from '../../../shared/telemetry/telemetry.gen'
-import * as errors from '../../../shared/errors'
-import * as timeoutUtils from '../../../shared/utilities/timeoutUtils'
-import { performanceTest } from '../../../shared/performance/performance'
-
-const mockCreateCodeScanResponse = {
- $response: {
- data: {
- jobId: 'jobId',
- status: 'Pending',
- },
- requestId: 'requestId',
- hasNextPage: () => false,
- error: undefined,
- nextPage: () => undefined,
- redirectCount: 0,
- retryCount: 0,
- httpResponse: new HttpResponse(),
- },
- jobId: 'jobId',
- status: 'Pending',
-}
-
-const mockCreateUploadUrlResponse = {
- $response: {
- data: {
- uploadId: 'uploadId',
- uploadUrl: 'uploadUrl',
- },
- requestId: 'requestId',
- hasNextPage: () => false,
- error: undefined,
- nextPage: () => undefined,
- redirectCount: 0,
- retryCount: 0,
- httpResponse: new HttpResponse(),
- },
- uploadId: 'uploadId',
- uploadUrl: 'https://test.com',
-}
-
-const mockGetCodeScanResponse = {
- $response: {
- data: {
- status: 'Completed',
- },
- requestId: 'requestId',
- hasNextPage: () => false,
- error: undefined,
- nextPage: () => undefined,
- redirectCount: 0,
- retryCount: 0,
- httpResponse: new HttpResponse(),
- },
- status: 'Completed',
-}
-
-const mockCodeScanFindings = JSON.stringify([
- {
- filePath: 'workspaceFolder/python3.7-plain-sam-app/hello_world/app.py',
- startLine: 1,
- endLine: 1,
- title: 'title',
- description: {
- text: 'text',
- markdown: 'markdown',
- },
- detectorId: 'detectorId',
- detectorName: 'detectorName',
- findingId: 'findingId',
- relatedVulnerabilities: [],
- severity: 'High',
- remediation: {
- recommendation: {
- text: 'text',
- url: 'url',
- },
- suggestedFixes: [],
- },
- codeSnippet: [],
- } satisfies model.RawCodeScanIssue,
-])
-
-const mockListCodeScanFindingsResponse = {
- $response: {
- data: {
- codeScanFindings: mockCodeScanFindings,
- },
- requestId: 'requestId',
- hasNextPage: () => false,
- error: undefined,
- nextPage: () => undefined,
- redirectCount: 0,
- retryCount: 0,
- httpResponse: new HttpResponse(),
- },
- codeScanFindings: mockCodeScanFindings,
-}
+} from '../../codewhisperer/models/constants'
+import * as model from '../../codewhisperer/models/model'
+import { CodewhispererSecurityScan } from '../../shared/telemetry/telemetry.gen'
+import * as errors from '../../shared/errors'
+import * as timeoutUtils from '../../shared/utilities/timeoutUtils'
+import { createClient, mockGetCodeScanResponse } from './testUtil'
let extensionContext: FakeExtensionContext
let mockSecurityPanelViewProvider: SecurityPanelViewProvider
@@ -155,15 +53,6 @@ describe('startSecurityScan', function () {
after(async function () {
await closeAllEditors()
})
- const createClient = () => {
- const mockClient = stub(DefaultCodeWhispererClient)
-
- mockClient.createCodeScan.resolves(mockCreateCodeScanResponse)
- mockClient.createUploadUrl.resolves(mockCreateUploadUrlResponse)
- mockClient.getCodeScan.resolves(mockGetCodeScanResponse)
- mockClient.listCodeScanFindings.resolves(mockListCodeScanFindingsResponse)
- return mockClient
- }
const openTestFile = async (filePath: string) => {
const doc = await vscode.workspace.openTextDocument(filePath)
@@ -454,78 +343,3 @@ describe('startSecurityScan', function () {
} as unknown as CodewhispererSecurityScan)
})
})
-
-describe('startSecurityScanPerformanceTest', function () {
- beforeEach(async function () {
- extensionContext = await FakeExtensionContext.create()
- mockSecurityPanelViewProvider = new SecurityPanelViewProvider(extensionContext)
- const folder = await createTestWorkspaceFolder()
- const mockFilePath = join(folder.uri.fsPath, 'app.py')
- await toFile('hello_world', mockFilePath)
- appCodePath = mockFilePath
- editor = await openTestFile(appCodePath)
- await model.CodeScansState.instance.setScansEnabled(false)
- sinon.stub(timeoutUtils, 'sleep')
- })
-
- afterEach(function () {
- sinon.restore()
- })
-
- after(async function () {
- await closeAllEditors()
- })
-
- const createClient = () => {
- const mockClient = stub(DefaultCodeWhispererClient)
- mockClient.createCodeScan.resolves(mockCreateCodeScanResponse)
- mockClient.createUploadUrl.resolves(mockCreateUploadUrlResponse)
- mockClient.getCodeScan.resolves(mockGetCodeScanResponse)
- mockClient.listCodeScanFindings.resolves(mockListCodeScanFindingsResponse)
- return mockClient
- }
-
- const openTestFile = async (filePath: string) => {
- const doc = await vscode.workspace.openTextDocument(filePath)
- return await vscode.window.showTextDocument(doc, {
- selection: new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)),
- })
- }
-
- performanceTest({}, 'Should calculate cpu and memory usage for file scans', function () {
- return {
- setup: async () => {
- getFetchStubWithResponse({ status: 200, statusText: 'testing stub' })
- const commandSpy = sinon.spy(vscode.commands, 'executeCommand')
- const securityScanRenderSpy = sinon.spy(diagnosticsProvider, 'initSecurityScanRender')
- await model.CodeScansState.instance.setScansEnabled(true)
- return { commandSpy, securityScanRenderSpy }
- },
- execute: async () => {
- await startSecurityScan.startSecurityScan(
- mockSecurityPanelViewProvider,
- editor,
- createClient(),
- extensionContext,
- CodeAnalysisScope.FILE
- )
- },
- verify: ({
- commandSpy,
- securityScanRenderSpy,
- }: {
- commandSpy: sinon.SinonSpy
- securityScanRenderSpy: sinon.SinonSpy
- }) => {
- assert.ok(commandSpy.neverCalledWith('workbench.action.problems.focus'))
- assert.ok(securityScanRenderSpy.calledOnce)
- const warnings = getTestWindow().shownMessages.filter((m) => m.severity === SeverityLevel.Warning)
- assert.strictEqual(warnings.length, 0)
- assertTelemetry('codewhisperer_securityScan', {
- codewhispererCodeScanScope: 'FILE',
- passive: true,
- })
- },
- }
- })
-})
diff --git a/packages/core/src/test/codewhisperer/testUtil.ts b/packages/core/src/test/codewhisperer/testUtil.ts
index b2a347cad9f..e752c7c93b8 100644
--- a/packages/core/src/test/codewhisperer/testUtil.ts
+++ b/packages/core/src/test/codewhisperer/testUtil.ts
@@ -19,11 +19,13 @@ import globals from '../../shared/extensionGlobals'
import { session } from '../../codewhisperer/util/codeWhispererSession'
import { DefaultAWSClientBuilder, ServiceOptions } from '../../shared/awsClientBuilder'
import { FakeAwsContext } from '../utilities/fakeAwsContext'
-import { Service } from 'aws-sdk'
+import { HttpResponse, Service } from 'aws-sdk'
import userApiConfig = require('./../../codewhisperer/client/user-service-2.json')
import CodeWhispererUserClient = require('../../codewhisperer/client/codewhispereruserclient')
import { codeWhispererClient } from '../../codewhisperer/client/codewhisperer'
import { RecommendationHandler } from '../../codewhisperer/service/recommendationHandler'
+import * as model from '../../codewhisperer/models/model'
+import { stub } from '../utilities/stubber'
import { Dirent } from 'fs' // eslint-disable-line no-restricted-imports
export async function resetCodeWhispererGlobalVariables() {
@@ -208,6 +210,109 @@ export function createMockDirentFile(fileName: string): Dirent {
return dirent
}
+export const mockGetCodeScanResponse = {
+ $response: {
+ data: {
+ status: 'Completed',
+ },
+ requestId: 'requestId',
+ hasNextPage: () => false,
+ error: undefined,
+ nextPage: () => undefined,
+ redirectCount: 0,
+ retryCount: 0,
+ httpResponse: new HttpResponse(),
+ },
+ status: 'Completed',
+}
+
+export function createClient() {
+ const mockClient = stub(codewhispererClient.DefaultCodeWhispererClient)
+
+ const mockCreateCodeScanResponse = {
+ $response: {
+ data: {
+ jobId: 'jobId',
+ status: 'Pending',
+ },
+ requestId: 'requestId',
+ hasNextPage: () => false,
+ error: undefined,
+ nextPage: () => undefined,
+ redirectCount: 0,
+ retryCount: 0,
+ httpResponse: new HttpResponse(),
+ },
+ jobId: 'jobId',
+ status: 'Pending',
+ }
+ const mockCreateUploadUrlResponse = {
+ $response: {
+ data: {
+ uploadId: 'uploadId',
+ uploadUrl: 'uploadUrl',
+ },
+ requestId: 'requestId',
+ hasNextPage: () => false,
+ error: undefined,
+ nextPage: () => undefined,
+ redirectCount: 0,
+ retryCount: 0,
+ httpResponse: new HttpResponse(),
+ },
+ uploadId: 'uploadId',
+ uploadUrl: 'https://test.com',
+ }
+
+ const mockCodeScanFindings = JSON.stringify([
+ {
+ filePath: 'workspaceFolder/python3.7-plain-sam-app/hello_world/app.py',
+ startLine: 1,
+ endLine: 1,
+ title: 'title',
+ description: {
+ text: 'text',
+ markdown: 'markdown',
+ },
+ detectorId: 'detectorId',
+ detectorName: 'detectorName',
+ findingId: 'findingId',
+ relatedVulnerabilities: [],
+ severity: 'High',
+ remediation: {
+ recommendation: {
+ text: 'text',
+ url: 'url',
+ },
+ suggestedFixes: [],
+ },
+ codeSnippet: [],
+ } satisfies model.RawCodeScanIssue,
+ ])
+
+ const mockListCodeScanFindingsResponse = {
+ $response: {
+ data: {
+ codeScanFindings: mockCodeScanFindings,
+ },
+ requestId: 'requestId',
+ hasNextPage: () => false,
+ error: undefined,
+ nextPage: () => undefined,
+ redirectCount: 0,
+ retryCount: 0,
+ httpResponse: new HttpResponse(),
+ },
+ codeScanFindings: mockCodeScanFindings,
+ }
+
+ mockClient.createCodeScan.resolves(mockCreateCodeScanResponse)
+ mockClient.createUploadUrl.resolves(mockCreateUploadUrlResponse)
+ mockClient.getCodeScan.resolves(mockGetCodeScanResponse)
+ mockClient.listCodeScanFindings.resolves(mockListCodeScanFindingsResponse)
+ return mockClient
+}
+
export function aStringWithLineCount(lineCount: number, start: number = 0): string {
let s = ''
for (let i = start; i < start + lineCount; i++) {
diff --git a/packages/core/src/testInteg/buildIndex.test.ts b/packages/core/src/testInteg/perf/buildIndex.test.ts
similarity index 87%
rename from packages/core/src/testInteg/buildIndex.test.ts
rename to packages/core/src/testInteg/perf/buildIndex.test.ts
index 261327e5145..29f07e28587 100644
--- a/packages/core/src/testInteg/buildIndex.test.ts
+++ b/packages/core/src/testInteg/perf/buildIndex.test.ts
@@ -3,15 +3,15 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { performanceTest } from '../shared/performance/performance'
+import { performanceTest } from '../../shared/performance/performance'
import * as sinon from 'sinon'
import * as vscode from 'vscode'
import assert from 'assert'
-import { LspClient, LspController } from '../amazonq'
+import { LspClient, LspController } from '../../amazonq'
import { LanguageClient, ServerOptions } from 'vscode-languageclient'
-import { createTestWorkspace } from '../test/testUtil'
-import { GetUsageRequestType, IndexRequestType } from '../amazonq/lsp/types'
-import { getRandomString } from '../shared'
+import { createTestWorkspace } from '../../test/testUtil'
+import { GetUsageRequestType, IndexRequestType } from '../../amazonq/lsp/types'
+import { getRandomString } from '../../shared'
interface SetupResult {
clientReqStub: sinon.SinonStub
diff --git a/packages/core/src/testInteg/perf/collectFiles.test.ts b/packages/core/src/testInteg/perf/collectFiles.test.ts
new file mode 100644
index 00000000000..3e9914898f5
--- /dev/null
+++ b/packages/core/src/testInteg/perf/collectFiles.test.ts
@@ -0,0 +1,62 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import assert from 'assert'
+import * as vscode from 'vscode'
+import * as sinon from 'sinon'
+import { performanceTest } from '../../shared/performance/performance'
+import { createTestWorkspaceFolder, toFile } from '../../test/testUtil'
+import path from 'path'
+import { randomUUID } from '../../shared'
+import { collectFiles } from '../../shared/utilities/workspaceUtils'
+
+performanceTest(
+ // collecting all files in the workspace and zipping them is pretty resource intensive
+ {
+ linux: {
+ userCpuUsage: 85,
+ heapTotal: 2,
+ duration: 0.8,
+ },
+ },
+ 'calculate cpu and memory usage',
+ function () {
+ const totalFiles = 100
+ return {
+ setup: async () => {
+ const workspace = await createTestWorkspaceFolder()
+
+ sinon.stub(vscode.workspace, 'workspaceFolders').value([workspace])
+
+ const fileContent = randomUUID()
+ for (let x = 0; x < totalFiles; x++) {
+ await toFile(fileContent, path.join(workspace.uri.fsPath, `file.${x}`))
+ }
+
+ return {
+ workspace,
+ }
+ },
+ execute: async ({ workspace }: { workspace: vscode.WorkspaceFolder }) => {
+ return {
+ result: await collectFiles([workspace.uri.fsPath], [workspace], true),
+ }
+ },
+ verify: (
+ _: { workspace: vscode.WorkspaceFolder },
+ { result }: { result: Awaited> }
+ ) => {
+ assert.deepStrictEqual(result.length, totalFiles)
+ const sortedFiles = [...result].sort((a, b) => {
+ const numA = parseInt(a.relativeFilePath.split('.')[1])
+ const numB = parseInt(b.relativeFilePath.split('.')[1])
+ return numA - numB
+ })
+ for (let x = 0; x < totalFiles; x++) {
+ assert.deepStrictEqual(sortedFiles[x].relativeFilePath, `file.${x}`)
+ }
+ },
+ }
+ }
+)
diff --git a/packages/core/src/test/amazonqFeatureDev/prepareRepoData.test.ts b/packages/core/src/testInteg/perf/prepareRepoData.test.ts
similarity index 97%
rename from packages/core/src/test/amazonqFeatureDev/prepareRepoData.test.ts
rename to packages/core/src/testInteg/perf/prepareRepoData.test.ts
index 50a4093ceca..3e10a1bec42 100644
--- a/packages/core/src/test/amazonqFeatureDev/prepareRepoData.test.ts
+++ b/packages/core/src/testInteg/perf/prepareRepoData.test.ts
@@ -5,7 +5,7 @@
import assert from 'assert'
import { WorkspaceFolder } from 'vscode'
import { performanceTest } from '../../shared/performance/performance'
-import { createTestWorkspace } from '../testUtil'
+import { createTestWorkspace } from '../../test/testUtil'
import { prepareRepoData, TelemetryHelper } from '../../amazonqFeatureDev'
import { AmazonqCreateUpload, getRandomString } from '../../shared'
import { Span } from '../../shared/telemetry'
diff --git a/packages/core/src/testInteg/perf/startSecurityScan.test.ts b/packages/core/src/testInteg/perf/startSecurityScan.test.ts
new file mode 100644
index 00000000000..79209322d31
--- /dev/null
+++ b/packages/core/src/testInteg/perf/startSecurityScan.test.ts
@@ -0,0 +1,96 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import * as vscode from 'vscode'
+import * as sinon from 'sinon'
+import * as startSecurityScan from '../../codewhisperer/commands/startSecurityScan'
+import * as diagnosticsProvider from '../../codewhisperer/service/diagnosticsProvider'
+import * as model from '../../codewhisperer/models/model'
+import * as timeoutUtils from '../../shared/utilities/timeoutUtils'
+import assert from 'assert'
+import { SecurityPanelViewProvider } from '../../codewhisperer/views/securityPanelViewProvider'
+import { FakeExtensionContext } from '../../test/fakeExtensionContext'
+import { join } from 'path'
+import {
+ assertTelemetry,
+ closeAllEditors,
+ createTestWorkspaceFolder,
+ getFetchStubWithResponse,
+ toFile,
+} from '../../test/testUtil'
+import { getTestWindow } from '../../test/shared/vscode/window'
+import { SeverityLevel } from '../../test/shared/vscode/message'
+import { CodeAnalysisScope } from '../../codewhisperer'
+import { performanceTest } from '../../shared/performance/performance'
+import { createClient } from '../../test/codewhisperer/testUtil'
+
+describe('startSecurityScanPerformanceTest', function () {
+ let extensionContext: FakeExtensionContext
+ let mockSecurityPanelViewProvider: SecurityPanelViewProvider
+ let appCodePath: string
+ let editor: vscode.TextEditor
+ beforeEach(async function () {
+ extensionContext = await FakeExtensionContext.create()
+ mockSecurityPanelViewProvider = new SecurityPanelViewProvider(extensionContext)
+ const folder = await createTestWorkspaceFolder()
+ const mockFilePath = join(folder.uri.fsPath, 'app.py')
+ await toFile('hello_world', mockFilePath)
+ appCodePath = mockFilePath
+ editor = await openTestFile(appCodePath)
+ await model.CodeScansState.instance.setScansEnabled(false)
+ sinon.stub(timeoutUtils, 'sleep')
+ })
+
+ afterEach(function () {
+ sinon.restore()
+ })
+
+ after(async function () {
+ await closeAllEditors()
+ })
+
+ const openTestFile = async (filePath: string) => {
+ const doc = await vscode.workspace.openTextDocument(filePath)
+ return await vscode.window.showTextDocument(doc, {
+ selection: new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)),
+ })
+ }
+
+ performanceTest({}, 'Should calculate cpu and memory usage for file scans', function () {
+ return {
+ setup: async () => {
+ getFetchStubWithResponse({ status: 200, statusText: 'testing stub' })
+ const commandSpy = sinon.spy(vscode.commands, 'executeCommand')
+ const securityScanRenderSpy = sinon.spy(diagnosticsProvider, 'initSecurityScanRender')
+ await model.CodeScansState.instance.setScansEnabled(true)
+ return { commandSpy, securityScanRenderSpy }
+ },
+ execute: async () => {
+ await startSecurityScan.startSecurityScan(
+ mockSecurityPanelViewProvider,
+ editor,
+ createClient(),
+ extensionContext,
+ CodeAnalysisScope.FILE
+ )
+ },
+ verify: ({
+ commandSpy,
+ securityScanRenderSpy,
+ }: {
+ commandSpy: sinon.SinonSpy
+ securityScanRenderSpy: sinon.SinonSpy
+ }) => {
+ assert.ok(commandSpy.neverCalledWith('workbench.action.problems.focus'))
+ assert.ok(securityScanRenderSpy.calledOnce)
+ const warnings = getTestWindow().shownMessages.filter((m) => m.severity === SeverityLevel.Warning)
+ assert.strictEqual(warnings.length, 0)
+ assertTelemetry('codewhisperer_securityScan', {
+ codewhispererCodeScanScope: 'FILE',
+ passive: true,
+ })
+ },
+ }
+ })
+})
diff --git a/packages/core/src/testInteg/tryInstallLsp.test.ts b/packages/core/src/testInteg/perf/tryInstallLsp.test.ts
similarity index 94%
rename from packages/core/src/testInteg/tryInstallLsp.test.ts
rename to packages/core/src/testInteg/perf/tryInstallLsp.test.ts
index 9a5f8b808db..7cc9c18b2c4 100644
--- a/packages/core/src/testInteg/tryInstallLsp.test.ts
+++ b/packages/core/src/testInteg/perf/tryInstallLsp.test.ts
@@ -7,10 +7,10 @@ import sinon from 'sinon'
import { Content } from 'aws-sdk/clients/codecommit'
import AdmZip from 'adm-zip'
import path from 'path'
-import { LspController } from '../amazonq'
-import { fs, getRandomString, globals } from '../shared'
-import { createTestWorkspace } from '../test/testUtil'
-import { performanceTest } from '../shared/performance/performance'
+import { LspController } from '../../amazonq'
+import { fs, getRandomString, globals } from '../../shared'
+import { createTestWorkspace } from '../../test/testUtil'
+import { performanceTest } from '../../shared/performance/performance'
// fakeFileContent is matched to fakeQServerContent based on hash.
const fakeHash = '4eb2865c8f40a322aa04e17d8d83bdaa605d6f1cb363af615240a5442a010e0aef66e21bcf4c88f20fabff06efe8a214'
diff --git a/packages/core/src/testInteg/zipcode.test.ts b/packages/core/src/testInteg/perf/zipcode.test.ts
similarity index 87%
rename from packages/core/src/testInteg/zipcode.test.ts
rename to packages/core/src/testInteg/perf/zipcode.test.ts
index 685c26c02e8..1883861f222 100644
--- a/packages/core/src/testInteg/zipcode.test.ts
+++ b/packages/core/src/testInteg/perf/zipcode.test.ts
@@ -4,12 +4,12 @@
*/
import assert from 'assert'
import * as sinon from 'sinon'
-import { TransformByQState, ZipManifest } from '../codewhisperer'
-import { fs, getRandomString, globals } from '../shared'
-import { createTestWorkspace } from '../test/testUtil'
-import * as CodeWhispererConstants from '../codewhisperer/models/constants'
-import { performanceTest } from '../shared/performance/performance'
-import { zipCode } from '../codewhisperer/indexNode'
+import { TransformByQState, ZipManifest } from '../../codewhisperer'
+import { fs, getRandomString, globals } from '../../shared'
+import { createTestWorkspace } from '../../test/testUtil'
+import * as CodeWhispererConstants from '../../codewhisperer/models/constants'
+import { performanceTest } from '../../shared/performance/performance'
+import { zipCode } from '../../codewhisperer/indexNode'
interface SetupResult {
tempDir: string
diff --git a/packages/core/src/testInteg/shared/utilities/workspaceUtils.test.ts b/packages/core/src/testInteg/shared/utilities/workspaceUtils.test.ts
index 19dd0d965a1..95619f0599c 100644
--- a/packages/core/src/testInteg/shared/utilities/workspaceUtils.test.ts
+++ b/packages/core/src/testInteg/shared/utilities/workspaceUtils.test.ts
@@ -18,8 +18,6 @@ import globals from '../../../shared/extensionGlobals'
import { CodelensRootRegistry } from '../../../shared/fs/codelensRootRegistry'
import { createTestWorkspace, createTestWorkspaceFolder, toFile } from '../../../test/testUtil'
import sinon from 'sinon'
-import { performanceTest } from '../../../shared/performance/performance'
-import { randomUUID } from '../../../shared/crypto'
import { fs } from '../../../shared'
describe('findParentProjectFile', async function () {
@@ -326,56 +324,6 @@ describe('collectFiles', function () {
assert.deepStrictEqual(1, result.length)
assert.deepStrictEqual('non-license.md', result[0].relativeFilePath)
})
-
- performanceTest(
- // collecting all files in the workspace and zipping them is pretty resource intensive
- {
- linux: {
- userCpuUsage: 85,
- heapTotal: 2,
- duration: 0.8,
- },
- },
- 'calculate cpu and memory usage',
- function () {
- const totalFiles = 100
- return {
- setup: async () => {
- const workspace = await createTestWorkspaceFolder()
-
- sinon.stub(vscode.workspace, 'workspaceFolders').value([workspace])
-
- const fileContent = randomUUID()
- for (let x = 0; x < totalFiles; x++) {
- await toFile(fileContent, path.join(workspace.uri.fsPath, `file.${x}`))
- }
-
- return {
- workspace,
- }
- },
- execute: async ({ workspace }: { workspace: vscode.WorkspaceFolder }) => {
- return {
- result: await collectFiles([workspace.uri.fsPath], [workspace], true),
- }
- },
- verify: (
- _: { workspace: vscode.WorkspaceFolder },
- { result }: { result: Awaited> }
- ) => {
- assert.deepStrictEqual(result.length, totalFiles)
- const sortedFiles = [...result].sort((a, b) => {
- const numA = parseInt(a.relativeFilePath.split('.')[1])
- const numB = parseInt(b.relativeFilePath.split('.')[1])
- return numA - numB
- })
- for (let x = 0; x < totalFiles; x++) {
- assert.deepStrictEqual(sortedFiles[x].relativeFilePath, `file.${x}`)
- }
- },
- }
- }
- )
})
describe('getWorkspaceFoldersByPrefixes', function () {
From b944d5a9c9e1bde40cc1c115ea101c20f496a577 Mon Sep 17 00:00:00 2001
From: Hweinstock <42325418+Hweinstock@users.noreply.github.com>
Date: Mon, 14 Oct 2024 16:03:29 -0400
Subject: [PATCH 42/87] fix(test): fix flaky test related to ec2.updateStatus
(#5758)
## Problem
Follow up to: https://github.com/aws/aws-toolkit-vscode/pull/5698
Fix: https://github.com/aws/aws-toolkit-vscode/issues/5750
### First Problem Identified
The current tests in
https://github.com/aws/aws-toolkit-vscode/blob/master/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts
do not clear the `PollingSet` in between tests, therefore the timer
continues to run. This not only results in inconsistent state between
the items in the `PollingSet` and the items stored on the parent node
itself, but also allows the `PollingSet` action to trigger unexpectedly
in the middle of another test. When this happens, and the states don't
match, the `instanceId` from the `PollingSet` could potentially not be
found on the parent node, resulting in an undefined node. Then, calling
`updateStatus` on this node could explain the error.
For this error to happen, the `PollingSet` timer must trigger at a very
specific point (in the middle of another test), making it difficult to
debug and reproduce, and causing occasional flaky test failures.
### Second Problem Identified
The tests in
https://github.com/aws/aws-toolkit-vscode/blob/master/packages/core/src/awsService/ec2/explorer/ec2InstanceNode.ts
setup an unnatural state. The `testNode` sets its parent to the
`testParentNode`, but `testParentNode` is unaware this child exists.
This is problematic because when the child reports its status as pending
to the parent, the parent will throw an error since it can't find the
child. (example:
https://d1ihu6zq92vp9p.cloudfront.net/7b1e96c7-f2b7-4a8e-927c-b7aa84437960/report.html)
This type of state should not be allowed.
## Solution
### Solution to first problem
- clear the set, and the timer in-between each test.
### Solution to second problem
- make this confusing state impossible by adding the child to the parent
and parent to the child together.
- stub the resulting API call its makes.
- Disable the polling set timer in the second tests (via sinon stubbing)
since it isn't relevant to what's being tested.
### Tangential Work included in PR:
- Throw our own error when an `instanceId` is not in map on the
parentNode. This will make it easier to catch if something goes wrong.
- Clean up test file:
`src/test/awsService/ec2/explorer/ec2ParentNode.test.ts`.
---
License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---
.../ec2/explorer/ec2InstanceNode.ts | 3 +-
.../awsService/ec2/explorer/ec2ParentNode.ts | 22 ++++-
.../ec2/explorer/ec2InstanceNode.test.ts | 8 +-
.../ec2/explorer/ec2ParentNode.test.ts | 87 ++++++++++++-------
4 files changed, 85 insertions(+), 35 deletions(-)
diff --git a/packages/core/src/awsService/ec2/explorer/ec2InstanceNode.ts b/packages/core/src/awsService/ec2/explorer/ec2InstanceNode.ts
index ded19d5059a..7078b6bdb02 100644
--- a/packages/core/src/awsService/ec2/explorer/ec2InstanceNode.ts
+++ b/packages/core/src/awsService/ec2/explorer/ec2InstanceNode.ts
@@ -29,6 +29,7 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode
public readonly instance: SafeEc2Instance
) {
super('')
+ this.parent.addChild(this)
this.updateInstance(instance)
this.id = this.InstanceId
}
@@ -41,7 +42,7 @@ export class Ec2InstanceNode extends AWSTreeNodeBase implements AWSResourceNode
this.tooltip = `${this.name}\n${this.InstanceId}\n${this.instance.LastSeenStatus}\n${this.arn}`
if (this.isPending()) {
- this.parent.pollingSet.start(this.InstanceId)
+ this.parent.trackPendingNode(this.InstanceId)
}
}
diff --git a/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts b/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts
index f751f7e0c61..854e6eacd1c 100644
--- a/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts
+++ b/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts
@@ -41,6 +41,13 @@ export class Ec2ParentNode extends AWSTreeNodeBase {
})
}
+ public trackPendingNode(instanceId: string) {
+ if (!this.ec2InstanceNodes.has(instanceId)) {
+ throw new Error(`Attempt to track ec2 node ${instanceId} that isn't a child`)
+ }
+ this.pollingSet.start(instanceId)
+ }
+
public async updateChildren(): Promise {
const ec2Instances = await (await this.ec2Client.getInstances()).toMap((instance) => instance.InstanceId)
updateInPlace(
@@ -52,9 +59,18 @@ export class Ec2ParentNode extends AWSTreeNodeBase {
)
}
+ public getInstanceNode(instanceId: string): Ec2InstanceNode {
+ const childNode = this.ec2InstanceNodes.get(instanceId)
+ if (childNode) {
+ return childNode
+ } else {
+ throw new Error(`Node with id ${instanceId} from polling set not found`)
+ }
+ }
+
private async updatePendingNodes() {
for (const instanceId of this.pollingSet.values()) {
- const childNode = this.ec2InstanceNodes.get(instanceId)!
+ const childNode = this.getInstanceNode(instanceId)
await this.updatePendingNode(childNode)
}
}
@@ -71,6 +87,10 @@ export class Ec2ParentNode extends AWSTreeNodeBase {
this.ec2InstanceNodes = new Map()
}
+ public addChild(node: Ec2InstanceNode) {
+ this.ec2InstanceNodes.set(node.InstanceId, node)
+ }
+
public async refreshNode(): Promise {
await this.clearChildren()
await vscode.commands.executeCommand('aws.refreshAwsExplorerNode', this)
diff --git a/packages/core/src/test/awsService/ec2/explorer/ec2InstanceNode.test.ts b/packages/core/src/test/awsService/ec2/explorer/ec2InstanceNode.test.ts
index a5976ef8f01..5299d2a080d 100644
--- a/packages/core/src/test/awsService/ec2/explorer/ec2InstanceNode.test.ts
+++ b/packages/core/src/test/awsService/ec2/explorer/ec2InstanceNode.test.ts
@@ -12,6 +12,8 @@ import {
} from '../../../../awsService/ec2/explorer/ec2InstanceNode'
import { Ec2Client, SafeEc2Instance, getNameOfInstance } from '../../../../shared/clients/ec2Client'
import { Ec2ParentNode } from '../../../../awsService/ec2/explorer/ec2ParentNode'
+import * as sinon from 'sinon'
+import { PollingSet } from '../../../../shared/utilities/pollingSet'
describe('ec2InstanceNode', function () {
let testNode: Ec2InstanceNode
@@ -30,12 +32,16 @@ describe('ec2InstanceNode', function () {
],
LastSeenStatus: 'running',
}
+ sinon.stub(Ec2InstanceNode.prototype, 'updateStatus')
+ // Don't want to be polling here, that is tested in ../ec2ParentNode.test.ts
+ // disabled here for convenience (avoiding race conditions with timeout)
+ sinon.stub(PollingSet.prototype, 'start')
const testClient = new Ec2Client('')
const testParentNode = new Ec2ParentNode(testRegion, testPartition, testClient)
testNode = new Ec2InstanceNode(testParentNode, testClient, 'testRegion', 'testPartition', testInstance)
})
- this.beforeEach(function () {
+ beforeEach(function () {
testNode.updateInstance(testInstance)
})
diff --git a/packages/core/src/test/awsService/ec2/explorer/ec2ParentNode.test.ts b/packages/core/src/test/awsService/ec2/explorer/ec2ParentNode.test.ts
index cb25c9f885c..788c97db046 100644
--- a/packages/core/src/test/awsService/ec2/explorer/ec2ParentNode.test.ts
+++ b/packages/core/src/test/awsService/ec2/explorer/ec2ParentNode.test.ts
@@ -17,17 +17,14 @@ import { EC2 } from 'aws-sdk'
import { AsyncCollection } from '../../../../shared/utilities/asyncCollection'
import * as FakeTimers from '@sinonjs/fake-timers'
import { installFakeClock } from '../../../testUtil'
-import { PollingSet } from '../../../../shared/utilities/pollingSet'
describe('ec2ParentNode', function () {
let testNode: Ec2ParentNode
- let defaultInstances: SafeEc2Instance[]
let client: Ec2Client
let getInstanceStub: sinon.SinonStub<[filters?: EC2.Filter[] | undefined], Promise>>
let clock: FakeTimers.InstalledClock
let refreshStub: sinon.SinonStub<[], Promise>
- let clearTimerStub: sinon.SinonStub<[], void>
-
+ let statusUpdateStub: sinon.SinonStub<[status: string], Promise>
const testRegion = 'testRegion'
const testPartition = 'testPartition'
@@ -45,36 +42,19 @@ describe('ec2ParentNode', function () {
client = new Ec2Client(testRegion)
clock = installFakeClock()
refreshStub = sinon.stub(Ec2InstanceNode.prototype, 'refreshNode')
- clearTimerStub = sinon.stub(PollingSet.prototype, 'clearTimer')
- defaultInstances = [
- { Name: 'firstOne', InstanceId: '0', LastSeenStatus: 'running' },
- { Name: 'secondOne', InstanceId: '1', LastSeenStatus: 'running' },
- ]
+ statusUpdateStub = sinon.stub(Ec2Client.prototype, 'getInstanceStatus')
})
beforeEach(function () {
getInstanceStub = sinon.stub(Ec2Client.prototype, 'getInstances')
- defaultInstances = [
- { Name: 'firstOne', InstanceId: '0', LastSeenStatus: 'running' },
- { Name: 'secondOne', InstanceId: '1', LastSeenStatus: 'stopped' },
- ]
-
- getInstanceStub.callsFake(async () =>
- intoCollection(
- defaultInstances.map((instance) => ({
- InstanceId: instance.InstanceId,
- Tags: [{ Key: 'Name', Value: instance.Name }],
- }))
- )
- )
-
testNode = new Ec2ParentNode(testRegion, testPartition, client)
refreshStub.resetHistory()
- clearTimerStub.resetHistory()
})
afterEach(function () {
getInstanceStub.restore()
+ testNode.pollingSet.clear()
+ testNode.pollingSet.clearTimer()
})
after(function () {
@@ -91,10 +71,14 @@ describe('ec2ParentNode', function () {
})
it('has instance child nodes', async function () {
- getInstanceStub.resolves(mapToInstanceCollection(defaultInstances))
+ const instances = [
+ { Name: 'firstOne', InstanceId: '0', LastSeenStatus: 'running' },
+ { Name: 'secondOne', InstanceId: '1', LastSeenStatus: 'stopped' },
+ ]
+ getInstanceStub.resolves(mapToInstanceCollection(instances))
const childNodes = await testNode.getChildren()
- assert.strictEqual(childNodes.length, defaultInstances.length, 'Unexpected child count')
+ assert.strictEqual(childNodes.length, instances.length, 'Unexpected child count')
childNodes.forEach((node) =>
assert.ok(node instanceof Ec2InstanceNode, 'Expected child node to be Ec2InstanceNode')
@@ -151,14 +135,13 @@ describe('ec2ParentNode', function () {
]
getInstanceStub.resolves(mapToInstanceCollection(instances))
-
await testNode.updateChildren()
assert.strictEqual(testNode.pollingSet.size, 1)
getInstanceStub.restore()
})
it('does not refresh explorer when timer goes off if status unchanged', async function () {
- const statusUpdateStub = sinon.stub(Ec2Client.prototype, 'getInstanceStatus').resolves('pending')
+ statusUpdateStub = statusUpdateStub.resolves('pending')
const instances = [
{ Name: 'firstOne', InstanceId: '0', LastSeenStatus: 'pending' },
{ Name: 'secondOne', InstanceId: '1', LastSeenStatus: 'stopped' },
@@ -170,16 +153,56 @@ describe('ec2ParentNode', function () {
await testNode.updateChildren()
await clock.tickAsync(6000)
sinon.assert.notCalled(refreshStub)
- statusUpdateStub.restore()
getInstanceStub.restore()
})
it('does refresh explorer when timer goes and status changed', async function () {
+ statusUpdateStub = statusUpdateStub.resolves('running')
+ const instances = [{ Name: 'firstOne', InstanceId: '0', LastSeenStatus: 'pending' }]
+
+ getInstanceStub.resolves(mapToInstanceCollection(instances))
+ await testNode.updateChildren()
+
sinon.assert.notCalled(refreshStub)
- const statusUpdateStub = sinon.stub(Ec2Client.prototype, 'getInstanceStatus').resolves('running')
- testNode.pollingSet.add('0')
await clock.tickAsync(6000)
sinon.assert.called(refreshStub)
- statusUpdateStub.restore()
+ })
+
+ it('returns the node when in the map', async function () {
+ const instances = [{ Name: 'firstOne', InstanceId: 'node1', LastSeenStatus: 'pending' }]
+
+ getInstanceStub.resolves(mapToInstanceCollection(instances))
+ await testNode.updateChildren()
+ const node = testNode.getInstanceNode('node1')
+ assert.strictEqual(node.InstanceId, instances[0].InstanceId)
+ getInstanceStub.restore()
+ })
+
+ it('throws error when node not in map', async function () {
+ const instances = [{ Name: 'firstOne', InstanceId: 'node1', LastSeenStatus: 'pending' }]
+
+ getInstanceStub.resolves(mapToInstanceCollection(instances))
+ await testNode.updateChildren()
+ assert.throws(() => testNode.getInstanceNode('node2'))
+ getInstanceStub.restore()
+ })
+
+ it('adds node to polling set when asked to track it', async function () {
+ const instances = [{ Name: 'firstOne', InstanceId: 'node1', LastSeenStatus: 'pending' }]
+
+ getInstanceStub.resolves(mapToInstanceCollection(instances))
+ await testNode.updateChildren()
+ testNode.trackPendingNode('node1')
+ assert.strictEqual(testNode.pollingSet.size, 1)
+ getInstanceStub.restore()
+ })
+
+ it('throws error when asked to track non-child node', async function () {
+ const instances = [{ Name: 'firstOne', InstanceId: 'node1', LastSeenStatus: 'pending' }]
+
+ getInstanceStub.resolves(mapToInstanceCollection(instances))
+ await testNode.updateChildren()
+ assert.throws(() => testNode.trackPendingNode('node2'))
+ getInstanceStub.restore()
})
})
From dbd54c5f597c7c52762df0514930390f24b6ea5c Mon Sep 17 00:00:00 2001
From: Hweinstock <42325418+Hweinstock@users.noreply.github.com>
Date: Mon, 14 Oct 2024 16:11:11 -0400
Subject: [PATCH 43/87] test(amazonq): performance test for registerNewFiles
(#5727)
## Problem
Continuation of https://github.com/aws/aws-toolkit-vscode/pull/5670
## Solution
same as previous
---
License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---
.../amazonqFeatureDev/session/sessionState.ts | 2 +-
.../testInteg/perf/registerNewFiles.test.ts | 87 +++++++++++++++++++
2 files changed, 88 insertions(+), 1 deletion(-)
create mode 100644 packages/core/src/testInteg/perf/registerNewFiles.test.ts
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index 03b6e78e178..a523e373023 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -59,7 +59,7 @@ export class ConversationNotStartedState implements Omit {
+ return {
+ zipFilePath: `test-path-${i}`,
+ fileContent: 'x'.repeat(fileSize),
+ }
+ })
+}
+
+function performanceTestWrapper(label: string, numFiles: number, fileSize: number) {
+ const conversationId = 'test-conversation'
+ return performanceTest(
+ {
+ testRuns: 10,
+ linux: {
+ userCpuUsage: 200,
+ systemCpuUsage: 35,
+ heapTotal: 8,
+ },
+ darwin: {
+ userCpuUsage: 200,
+ systemCpuUsage: 35,
+ heapTotal: 8,
+ },
+ win32: {
+ userCpuUsage: 200,
+ systemCpuUsage: 35,
+ heapTotal: 8,
+ },
+ },
+ label,
+ function () {
+ return {
+ setup: async () => {
+ const testWorkspaceUri = vscode.Uri.file(getTestWorkspaceFolder())
+ const fileContents = getFileContents(numFiles, fileSize)
+ return {
+ workspace: {
+ uri: testWorkspaceUri,
+ name: 'test-workspace',
+ index: 0,
+ },
+ fileContents: fileContents,
+ }
+ },
+ execute: async (setup: SetupResult) => {
+ return registerNewFiles(
+ new VirtualFileSystem(),
+ setup.fileContents,
+ 'test-upload-id',
+ [setup.workspace],
+ conversationId
+ )
+ },
+ verify: async (_setup: SetupResult, result: NewFileInfo[]) => {
+ assert.strictEqual(result.length, numFiles)
+ },
+ }
+ }
+ )
+}
+
+describe('registerNewFiles', function () {
+ describe('performance tests', function () {
+ performanceTestWrapper('1x10MB', 1, 10000)
+ performanceTestWrapper('10x1000B', 10, 1000)
+ performanceTestWrapper('100x100B', 100, 100)
+ performanceTestWrapper('1000x10B', 1000, 10)
+ performanceTestWrapper('10000x1B', 10000, 1)
+ })
+})
From eedbda23fe129dfee930442c8da36276ab681980 Mon Sep 17 00:00:00 2001
From: Hweinstock <42325418+Hweinstock@users.noreply.github.com>
Date: Mon, 14 Oct 2024 17:27:08 -0400
Subject: [PATCH 44/87] test(amazonq): performance test for hashing file in
LSP. (#5720)
## Problem
continuation of performance test work:
https://github.com/aws/aws-toolkit-vscode/pull/5670
## Solution
same as previous.
CPU threshold set very high because it spikes pretty wildly on this one.

---
License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---
.../src/testInteg/perf/getFileSha384.test.ts | 61 +++++++++++++++++++
1 file changed, 61 insertions(+)
create mode 100644 packages/core/src/testInteg/perf/getFileSha384.test.ts
diff --git a/packages/core/src/testInteg/perf/getFileSha384.test.ts b/packages/core/src/testInteg/perf/getFileSha384.test.ts
new file mode 100644
index 00000000000..5072f448244
--- /dev/null
+++ b/packages/core/src/testInteg/perf/getFileSha384.test.ts
@@ -0,0 +1,61 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import assert from 'assert'
+import path from 'path'
+import { getTestWorkspaceFolder } from '../integrationTestsUtilities'
+import { fs, getRandomString } from '../../shared'
+import { LspController } from '../../amazonq'
+import { performanceTest } from '../../shared/performance/performance'
+
+function performanceTestWrapper(label: string, fileSize: number) {
+ return performanceTest(
+ {
+ testRuns: 1,
+ linux: {
+ userCpuUsage: 400,
+ systemCpuUsage: 35,
+ heapTotal: 4,
+ },
+ darwin: {
+ userCpuUsage: 400,
+ systemCpuUsage: 35,
+ heapTotal: 4,
+ },
+ win32: {
+ userCpuUsage: 400,
+ systemCpuUsage: 35,
+ heapTotal: 4,
+ },
+ },
+ label,
+ function () {
+ return {
+ setup: async () => {
+ const workspace = getTestWorkspaceFolder()
+ const fileContent = getRandomString(fileSize)
+ const testFile = path.join(workspace, 'test-file')
+ await fs.writeFile(testFile, fileContent)
+
+ return testFile
+ },
+ execute: async (testFile: string) => {
+ return await LspController.instance.getFileSha384(testFile)
+ },
+ verify: async (_testFile: string, result: string) => {
+ assert.strictEqual(result.length, 96)
+ },
+ }
+ }
+ )
+}
+
+describe('getFileSha384', function () {
+ describe('performance tests', function () {
+ performanceTestWrapper('1MB', 1000)
+ performanceTestWrapper('2MB', 2000)
+ performanceTestWrapper('4MB', 4000)
+ performanceTestWrapper('8MB', 8000)
+ })
+})
From fe4adf72be11efff58e8f1deebb7b43c01aef6c5 Mon Sep 17 00:00:00 2001
From: Hweinstock <42325418+Hweinstock@users.noreply.github.com>
Date: Mon, 14 Oct 2024 17:31:41 -0400
Subject: [PATCH 45/87] test(amazonq): implement performance test for
downloadExportResultArchieve. (#5710)
## Problem
Continuation of work here:
https://github.com/aws/aws-toolkit-vscode/pull/5670
## Solution
In this case `downloadExportResultArchieve` is reading a large
collection of objects into a buffer, then writing that buffer to a file.
We test this with varying collections of objects. We test 1x1KB,
10x100B, 100x10B, and 1000x1B.
---
License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---
.../perf/downloadExportResultArchive.test.ts | 98 +++++++++++++++++++
1 file changed, 98 insertions(+)
create mode 100644 packages/core/src/testInteg/perf/downloadExportResultArchive.test.ts
diff --git a/packages/core/src/testInteg/perf/downloadExportResultArchive.test.ts b/packages/core/src/testInteg/perf/downloadExportResultArchive.test.ts
new file mode 100644
index 00000000000..4f78fdc4b0e
--- /dev/null
+++ b/packages/core/src/testInteg/perf/downloadExportResultArchive.test.ts
@@ -0,0 +1,98 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import assert from 'assert'
+import { WorkspaceFolder } from 'vscode'
+import { ExportResultArchiveCommandInput } from '@amzn/codewhisperer-streaming'
+import * as sinon from 'sinon'
+import path from 'path'
+import { fs, getRandomString } from '../../shared'
+import { createTestWorkspace } from '../../test/testUtil'
+import { performanceTest } from '../../shared/performance/performance'
+import { downloadExportResultArchive } from '../../shared/utilities/download'
+
+interface SetupResult {
+ workspace: WorkspaceFolder
+ exportCommandInput: ExportResultArchiveCommandInput
+ writeFileStub: sinon.SinonStub
+ cwStreaming: any
+}
+
+interface FakeCommandOutput {
+ body: { binaryPayloadEvent: { bytes: Buffer } }[]
+}
+
+function generateCommandOutput(numPieces: number, pieceSize: number): FakeCommandOutput {
+ const body = Array.from({ length: numPieces }, (_, i) => {
+ return {
+ binaryPayloadEvent: {
+ bytes: Buffer.from(getRandomString(pieceSize)),
+ },
+ }
+ })
+ return { body }
+}
+
+async function setup(pieces: number, pieceSize: number): Promise {
+ // Force VSCode to find test workspace only to keep test contained and controlled.
+ const workspace = await createTestWorkspace(1, {})
+ const exportCommandInput = {} as ExportResultArchiveCommandInput
+ // Manutally stub the CodeWhispererStreaming to avoid constructor call.
+ const cwStreaming = { exportResultArchive: () => generateCommandOutput(pieces, pieceSize) }
+
+ const writeFileStub = sinon.stub(fs, 'writeFile')
+ return { workspace, exportCommandInput, writeFileStub, cwStreaming }
+}
+
+function perfTest(pieces: number, pieceSize: number, label: string) {
+ return performanceTest(
+ {
+ testRuns: 10,
+ linux: {
+ userCpuUsage: 200,
+ systemCpuUsage: 35,
+ heapTotal: 4,
+ },
+ darwin: {
+ userCpuUsage: 200,
+ systemCpuUsage: 35,
+ heapTotal: 4,
+ },
+ win32: {
+ userCpuUsage: 200,
+ systemCpuUsage: 35,
+ heapTotal: 4,
+ },
+ },
+ label,
+ function () {
+ return {
+ setup: async () => await setup(pieces, pieceSize),
+ execute: async ({ workspace, cwStreaming, exportCommandInput, writeFileStub }: SetupResult) => {
+ await downloadExportResultArchive(
+ cwStreaming,
+ exportCommandInput,
+ path.join(workspace.uri.fsPath, 'result')
+ )
+ },
+ verify: async (setup: SetupResult) => {
+ assert.ok(setup.writeFileStub.calledOnce)
+ assert.ok((setup.writeFileStub.firstCall.args[1] as Buffer).length === pieces * pieceSize)
+ },
+ }
+ }
+ )
+}
+
+describe('downloadExportResultArchive', function () {
+ describe('performanceTests', function () {
+ afterEach(function () {
+ sinon.restore()
+ })
+ perfTest(1, 1000, '1x1KB')
+ perfTest(10, 100, '10x100B')
+ perfTest(100, 10, '100x10B')
+ perfTest(1000, 1, '1000x1B')
+ })
+})
From 3e355accf7fd66343d753c0a6190d4db06f45e45 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 15 Oct 2024 11:01:10 -0400
Subject: [PATCH 46/87] deps: bump prettier from 3.3.2 to 3.3.3 (#5781)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [prettier](https://github.com/prettier/prettier) from 3.3.2 to
3.3.3.
Release notes
Sourced from prettier's
releases.
3.3.3
🔗 Changelog
Changelog
Sourced from prettier's
changelog.
3.3.3
diff
Add parentheses for nullish coalescing in ternary (#16391
by @​cdignam-segment)
This change adds clarity to operator precedence.
// Input
foo ? bar ?? foo : baz;
foo ?? bar ? a : b;
a ? b : foo ?? bar;
// Prettier 3.3.2
foo ? bar ?? foo : baz;
foo ?? bar ? a : b;
a ? b : foo ?? bar;
// Prettier 3.3.3
foo ? (bar ?? foo) : baz;
(foo ?? bar) ? a : b;
a ? b : (foo ?? bar);
Add parentheses for decorator expressions (#16458
by @​y-schneider)
Prevent parentheses around member expressions or tagged template
literals from being removed to follow the stricter parsing rules of
TypeScript 5.5.
// Input
@(foo`tagged template`)
class X {}
// Prettier 3.3.2
@​footagged
template
class X {}
// Prettier 3.3.3
@(footagged template)
class X {}
Adds support for Angular v18 @let declaration
syntax.
Please see the following code example. The @let
declaration allows you to define local variables within the
template:
... (truncated)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
---------
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Maxim Hayes
---
package-lock.json | 13 +++++++------
package.json | 2 +-
.../src/amazonqFeatureDev/session/sessionState.ts | 4 ++--
packages/core/src/auth/connection.ts | 2 +-
.../awsService/iot/explorer/iotCertificateNode.ts | 2 +-
.../src/awsService/iot/explorer/iotPolicyNode.ts | 2 +-
packages/core/src/codecatalyst/devEnv.ts | 2 +-
.../codewhisperer/service/securityScanHandler.ts | 2 +-
packages/core/src/lambda/models/samLambdaRuntime.ts | 2 +-
packages/core/src/shared/errors.ts | 2 +-
.../shared/resourcefetcher/httpResourceFetcher.ts | 2 +-
.../core/src/shared/sam/debugger/awsSamDebugger.ts | 4 ++--
packages/core/src/webviews/main.ts | 2 +-
13 files changed, 21 insertions(+), 20 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index bd06c2c49e5..88196017184 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -38,7 +38,7 @@
"eslint-plugin-security-node": "^1.1.4",
"eslint-plugin-unicorn": "^54.0.0",
"husky": "^9.0.7",
- "prettier": "^3.3.2",
+ "prettier": "^3.3.3",
"prettier-plugin-sh": "^0.14.0",
"pretty-quick": "^4.0.0",
"ts-node": "^10.9.1",
@@ -15405,9 +15405,10 @@
}
},
"node_modules/prettier": {
- "version": "3.3.2",
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
+ "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
"dev": true,
- "license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -19267,7 +19268,7 @@
"devDependencies": {},
"engines": {
"npm": "^10.1.0",
- "vscode": "^1.68.0"
+ "vscode": "^1.83.0"
}
},
"packages/core": {
@@ -19396,7 +19397,7 @@
},
"engines": {
"npm": "^10.1.0",
- "vscode": "^1.68.0"
+ "vscode": "^1.83.0"
}
},
"packages/core/node_modules/@types/node": {
@@ -19428,7 +19429,7 @@
"devDependencies": {},
"engines": {
"npm": "^10.1.0",
- "vscode": "^1.68.0"
+ "vscode": "^1.83.0"
}
},
"plugins/eslint-plugin-aws-toolkits": {
diff --git a/package.json b/package.json
index c4b768d96e6..8d82cac692f 100644
--- a/package.json
+++ b/package.json
@@ -58,7 +58,7 @@
"eslint-plugin-security-node": "^1.1.4",
"eslint-plugin-unicorn": "^54.0.0",
"husky": "^9.0.7",
- "prettier": "^3.3.2",
+ "prettier": "^3.3.3",
"prettier-plugin-sh": "^0.14.0",
"pretty-quick": "^4.0.0",
"ts-node": "^10.9.1",
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index a523e373023..36b659629f7 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -79,10 +79,10 @@ export function registerNewFiles(
const folder =
workspaceFolderPrefixes === undefined
? workspaceFolders[0]
- : workspaceFolderPrefixes[prefix] ??
+ : (workspaceFolderPrefixes[prefix] ??
workspaceFolderPrefixes[
Object.values(workspaceFolderPrefixes).find((val) => val.index === 0)?.name ?? ''
- ]
+ ])
if (folder === undefined) {
telemetry.toolkit_trackScenario.emit({
count: 1,
diff --git a/packages/core/src/auth/connection.ts b/packages/core/src/auth/connection.ts
index 29ab6d1b8ee..294fb141747 100644
--- a/packages/core/src/auth/connection.ts
+++ b/packages/core/src/auth/connection.ts
@@ -73,7 +73,7 @@ export function hasScopes(target: SsoConnection | SsoProfile | string[], scopes:
* Not optimized, but the set of possible scopes is currently very small (< 8)
*/
export function hasExactScopes(target: SsoConnection | SsoProfile | string[], scopes: string[]): boolean {
- const targetScopes = Array.isArray(target) ? target : target.scopes ?? []
+ const targetScopes = Array.isArray(target) ? target : (target.scopes ?? [])
return scopes.length === targetScopes.length && scopes.every((s) => targetScopes.includes(s))
}
diff --git a/packages/core/src/awsService/iot/explorer/iotCertificateNode.ts b/packages/core/src/awsService/iot/explorer/iotCertificateNode.ts
index 64d1cc3ae35..0009d057833 100644
--- a/packages/core/src/awsService/iot/explorer/iotCertificateNode.ts
+++ b/packages/core/src/awsService/iot/explorer/iotCertificateNode.ts
@@ -50,7 +50,7 @@ export abstract class IotCertificateNode extends AWSTreeNodeBase implements AWSR
this.certificate.id,
this.certificate.activeStatus,
formatLocalized(this.certificate.creationDate),
- things?.length ?? 0 > 0 ? `\nAttached to: ${things!.join(', ')}` : ''
+ (things?.length ?? 0 > 0) ? `\nAttached to: ${things!.join(', ')}` : ''
)
this.iconPath = getIcon('aws-iot-certificate')
this.description = `\t[${this.certificate.activeStatus}]`
diff --git a/packages/core/src/awsService/iot/explorer/iotPolicyNode.ts b/packages/core/src/awsService/iot/explorer/iotPolicyNode.ts
index 790a5864e6f..3f9b7003d60 100644
--- a/packages/core/src/awsService/iot/explorer/iotPolicyNode.ts
+++ b/packages/core/src/awsService/iot/explorer/iotPolicyNode.ts
@@ -39,7 +39,7 @@ export class IotPolicyNode extends AWSTreeNodeBase implements AWSResourceNode {
'AWS.explorerNode.iot.policyToolTip',
'{0}{1}',
policy.name,
- certs?.length ?? 0 > 0 ? `\nAttached to: ${certs?.join(', ')}` : ''
+ (certs?.length ?? 0 > 0) ? `\nAttached to: ${certs?.join(', ')}` : ''
)
this.iconPath = getIcon('aws-iot-policy')
this.contextValue = 'awsIotPolicyNode.Certificates'
diff --git a/packages/core/src/codecatalyst/devEnv.ts b/packages/core/src/codecatalyst/devEnv.ts
index cd45b353358..44db151d8de 100644
--- a/packages/core/src/codecatalyst/devEnv.ts
+++ b/packages/core/src/codecatalyst/devEnv.ts
@@ -98,7 +98,7 @@ export class DevEnvActivityStarter {
}
// If user is not authenticated, assume 15 minutes.
const inactivityTimeoutMin =
- devenvTimeoutMs > 0 ? devenvTimeoutMs / 60000 : thisDevenv?.summary.inactivityTimeoutMinutes ?? 15
+ devenvTimeoutMs > 0 ? devenvTimeoutMs / 60000 : (thisDevenv?.summary.inactivityTimeoutMinutes ?? 15)
if (!shouldSendActivity(inactivityTimeoutMin)) {
getLogger().info(
`codecatalyst: disabling DevEnvActivity heartbeat: configured to never timeout (inactivityTimeoutMinutes=${inactivityTimeoutMin})`
diff --git a/packages/core/src/codewhisperer/service/securityScanHandler.ts b/packages/core/src/codewhisperer/service/securityScanHandler.ts
index d01e1050f97..86480f0766e 100644
--- a/packages/core/src/codewhisperer/service/securityScanHandler.ts
+++ b/packages/core/src/codewhisperer/service/securityScanHandler.ts
@@ -310,7 +310,7 @@ export async function uploadArtifactToS3(
)
const errorMessage = getTelemetryReasonDesc(error)?.includes(`"PUT" request failed with code "403"`)
? `"PUT" request failed with code "403"`
- : getTelemetryReasonDesc(error) ?? 'Security scan failed.'
+ : (getTelemetryReasonDesc(error) ?? 'Security scan failed.')
throw new UploadArtifactToS3Error(errorMessage)
}
diff --git a/packages/core/src/lambda/models/samLambdaRuntime.ts b/packages/core/src/lambda/models/samLambdaRuntime.ts
index 42a8681c672..754d910a24e 100644
--- a/packages/core/src/lambda/models/samLambdaRuntime.ts
+++ b/packages/core/src/lambda/models/samLambdaRuntime.ts
@@ -258,7 +258,7 @@ export function createRuntimeQuickPick(params: {
totalSteps?: number
}): QuickPickPrompter {
const zipRuntimes = params.runtimeFamily
- ? getRuntimesForFamily(params.runtimeFamily) ?? samLambdaCreatableRuntimes()
+ ? (getRuntimesForFamily(params.runtimeFamily) ?? samLambdaCreatableRuntimes())
: samLambdaCreatableRuntimes()
const zipRuntimeItems = zipRuntimes
diff --git a/packages/core/src/shared/errors.ts b/packages/core/src/shared/errors.ts
index 6e4845dabee..09597a8285f 100644
--- a/packages/core/src/shared/errors.ts
+++ b/packages/core/src/shared/errors.ts
@@ -454,7 +454,7 @@ export function scrubNames(s: string, username?: string) {
* @param err Error object, or message text
*/
export function getTelemetryReasonDesc(err: unknown | undefined): string | undefined {
- const m = typeof err === 'string' ? err : getErrorMsg(err as Error, true) ?? ''
+ const m = typeof err === 'string' ? err : (getErrorMsg(err as Error, true) ?? '')
const msg = scrubNames(m, _username)
// Truncate message as these strings can be very long.
diff --git a/packages/core/src/shared/resourcefetcher/httpResourceFetcher.ts b/packages/core/src/shared/resourcefetcher/httpResourceFetcher.ts
index 68c513c4747..f2da4ba98aa 100644
--- a/packages/core/src/shared/resourcefetcher/httpResourceFetcher.ts
+++ b/packages/core/src/shared/resourcefetcher/httpResourceFetcher.ts
@@ -138,7 +138,7 @@ export class HttpResourceFetcher implements ResourceFetcher {
}
private logText(): string {
- return this.params.showUrl ? this.url : this.params.friendlyName ?? 'resource from URL'
+ return this.params.showUrl ? this.url : (this.params.friendlyName ?? 'resource from URL')
}
private logCancellation(event: CancelEvent) {
diff --git a/packages/core/src/shared/sam/debugger/awsSamDebugger.ts b/packages/core/src/shared/sam/debugger/awsSamDebugger.ts
index babdf182b8d..49b3b186b0f 100644
--- a/packages/core/src/shared/sam/debugger/awsSamDebugger.ts
+++ b/packages/core/src/shared/sam/debugger/awsSamDebugger.ts
@@ -267,11 +267,11 @@ export class SamDebugConfigProvider implements vscode.DebugConfigurationProvider
if (resource) {
// we do not know enough to populate the runtime field for Image-based Lambdas
const runtimeName = CloudFormation.isZipLambdaResource(resource?.Properties)
- ? CloudFormation.getStringForProperty(
+ ? (CloudFormation.getStringForProperty(
resource?.Properties,
'Runtime',
templateDatum.item
- ) ?? ''
+ ) ?? '')
: ''
configs.push(
createTemplateAwsSamDebugConfig(
diff --git a/packages/core/src/webviews/main.ts b/packages/core/src/webviews/main.ts
index 59f45758271..379dc869fd8 100644
--- a/packages/core/src/webviews/main.ts
+++ b/packages/core/src/webviews/main.ts
@@ -349,7 +349,7 @@ function createWebviewPanel(ctx: vscode.ExtensionContext, params: WebviewPanelPa
const viewColumn =
isCloud9() && params.viewColumn === vscode.ViewColumn.Beside
? vscode.ViewColumn.Two
- : params.viewColumn ?? vscode.ViewColumn.Active
+ : (params.viewColumn ?? vscode.ViewColumn.Active)
const panel = vscode.window.createWebviewPanel(
params.id,
From 9f368dae1cf27a4595244e0e6cd166661487b5b6 Mon Sep 17 00:00:00 2001
From: chengoramazon <143654391+chengoramazon@users.noreply.github.com>
Date: Tue, 15 Oct 2024 15:45:06 -0400
Subject: [PATCH 47/87] refactor(q-feature-dev): remove errorName from Error
classes #5769
## Problem
- Static `errorName` is unnecessary in feature dev error classes. It
also causes error code mismatches some of errorName which result in
wrong state.
## Solution
- Remove `errorName` from error classes
- use class name match to ensure error handled correctly.
- Add unit tests to cover all the error cases.
---
.../controllers/chat/controller.ts | 30 ++--
.../controllers/chat/messenger/messenger.ts | 1 +
packages/core/src/amazonqFeatureDev/errors.ts | 27 +---
.../controllers/chat/controller.test.ts | 130 ++++++++++++++++--
4 files changed, 138 insertions(+), 50 deletions(-)
diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
index 3dce78c8233..bf66fe77a4b 100644
--- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
+++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
@@ -214,14 +214,15 @@ export class FeatureDevController {
)
let defaultMessage
- const isDenyListedError = denyListedErrors.some((err) => errorMessage.includes(err))
+ const isDenyListedError = denyListedErrors.some((denyListedError) => err.message.includes(denyListedError))
- switch (err.code) {
- case ContentLengthError.errorName:
+ switch (err.constructor.name) {
+ case ContentLengthError.name:
this.messenger.sendAnswer({
type: 'answer',
tabID: message.tabID,
message: err.message + messageWithConversationId(session?.conversationIdUnsafe),
+ canBeVoted: true,
})
this.messenger.sendAnswer({
type: 'system-prompt',
@@ -235,14 +236,14 @@ export class FeatureDevController {
],
})
break
- case MonthlyConversationLimitError.errorName:
+ case MonthlyConversationLimitError.name:
this.messenger.sendMonthlyLimitError(message.tabID)
break
- case FeatureDevServiceError.errorName:
- case UploadCodeError.errorName:
- case UserMessageNotFoundError.errorName:
- case TabIdNotFoundError.errorName:
- case PrepareRepoFailedError.errorName:
+ case FeatureDevServiceError.name:
+ case UploadCodeError.name:
+ case UserMessageNotFoundError.name:
+ case TabIdNotFoundError.name:
+ case PrepareRepoFailedError.name:
this.messenger.sendErrorMessage(
errorMessage,
message.tabID,
@@ -250,11 +251,11 @@ export class FeatureDevController {
session?.conversationIdUnsafe
)
break
- case PromptRefusalException.errorName:
- case ZipFileError.errorName:
+ case PromptRefusalException.name:
+ case ZipFileError.name:
this.messenger.sendErrorMessage(errorMessage, message.tabID, 0, session?.conversationIdUnsafe, true)
break
- case NoChangeRequiredException.errorName:
+ case NoChangeRequiredException.name:
this.messenger.sendAnswer({
type: 'answer',
tabID: message.tabID,
@@ -263,11 +264,12 @@ export class FeatureDevController {
})
// Allow users to re-work the task description.
return this.newTask(message)
- case CodeIterationLimitError.errorName:
+ case CodeIterationLimitError.name:
this.messenger.sendAnswer({
type: 'answer',
tabID: message.tabID,
message: err.message + messageWithConversationId(session?.conversationIdUnsafe),
+ canBeVoted: true,
})
this.messenger.sendAnswer({
type: 'system-prompt',
@@ -282,7 +284,7 @@ export class FeatureDevController {
],
})
break
- case UploadURLExpired.errorName:
+ case UploadURLExpired.name:
this.messenger.sendAnswer({
type: 'answer',
tabID: message.tabID,
diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/messenger/messenger.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/messenger/messenger.ts
index 78d436a7e34..cac7f6ca571 100644
--- a/packages/core/src/amazonqFeatureDev/controllers/chat/messenger/messenger.ts
+++ b/packages/core/src/amazonqFeatureDev/controllers/chat/messenger/messenger.ts
@@ -85,6 +85,7 @@ export class Messenger {
type: 'answer',
tabID: tabID,
message: showDefaultMessage ? errorMessage : i18n('AWS.amazonq.featureDev.error.technicalDifficulties'),
+ canBeVoted: true,
})
this.sendFeedback(tabID)
return
diff --git a/packages/core/src/amazonqFeatureDev/errors.ts b/packages/core/src/amazonqFeatureDev/errors.ts
index b2152e73863..06e994080f3 100644
--- a/packages/core/src/amazonqFeatureDev/errors.ts
+++ b/packages/core/src/amazonqFeatureDev/errors.ts
@@ -17,8 +17,6 @@ export class ConversationIdNotFoundError extends ToolkitError {
}
export class TabIdNotFoundError extends ToolkitError {
- static errorName = 'TabIdNotFoundError'
-
constructor() {
super(i18n('AWS.amazonq.featureDev.error.tabIdNotFoundError'), {
code: 'TabIdNotFound',
@@ -26,12 +24,6 @@ export class TabIdNotFoundError extends ToolkitError {
}
}
-export class PanelLoadError extends ToolkitError {
- constructor() {
- super(`${featureName} UI panel failed to load`, { code: 'PanelLoadFailed' })
- }
-}
-
export class WorkspaceFolderNotFoundError extends ToolkitError {
constructor() {
super(i18n('AWS.amazonq.featureDev.error.workspaceFolderNotFoundError'), {
@@ -41,7 +33,6 @@ export class WorkspaceFolderNotFoundError extends ToolkitError {
}
export class UserMessageNotFoundError extends ToolkitError {
- static errorName = 'UserMessageNotFoundError'
constructor() {
super(i18n('AWS.amazonq.featureDev.error.userMessageNotFoundError'), {
code: 'MessageNotFound',
@@ -58,7 +49,6 @@ export class SelectedFolderNotInWorkspaceFolderError extends ToolkitError {
}
export class PromptRefusalException extends ToolkitError {
- static errorName = 'PromptRefusalException'
constructor() {
super(i18n('AWS.amazonq.featureDev.error.promptRefusalException'), {
code: 'PromptRefusalException',
@@ -67,7 +57,6 @@ export class PromptRefusalException extends ToolkitError {
}
export class NoChangeRequiredException extends ToolkitError {
- static errorName = 'NoChangeRequiredException'
constructor() {
super(i18n('AWS.amazonq.featureDev.error.noChangeRequiredException'), {
code: 'NoChangeRequiredException',
@@ -76,14 +65,12 @@ export class NoChangeRequiredException extends ToolkitError {
}
export class FeatureDevServiceError extends ToolkitError {
- static errorName = 'FeatureDevServiceError'
constructor(message: string, code: string) {
super(message, { code })
}
}
export class PrepareRepoFailedError extends ToolkitError {
- static errorName = 'PrepareRepoFailedError'
constructor() {
super(i18n('AWS.amazonq.featureDev.error.prepareRepoFailedError'), {
code: 'PrepareRepoFailed',
@@ -92,14 +79,12 @@ export class PrepareRepoFailedError extends ToolkitError {
}
export class UploadCodeError extends ToolkitError {
- static errorName = 'UploadCodeError'
constructor(statusCode: string) {
super(uploadCodeError, { code: `UploadCode-${statusCode}` })
}
}
export class UploadURLExpired extends ToolkitError {
- static errorName = 'UploadURLExpired'
constructor() {
super(i18n('AWS.amazonq.featureDev.error.uploadURLExpired'), { code: 'UploadURLExpired' })
}
@@ -112,30 +97,26 @@ export class IllegalStateTransition extends ToolkitError {
}
export class ContentLengthError extends ToolkitError {
- static errorName = 'ContentLengthError'
constructor() {
- super(i18n('AWS.amazonq.featureDev.error.contentLengthError'), { code: ContentLengthError.errorName })
+ super(i18n('AWS.amazonq.featureDev.error.contentLengthError'), { code: ContentLengthError.name })
}
}
export class ZipFileError extends ToolkitError {
- static errorName = 'ZipFileError'
constructor() {
- super(i18n('AWS.amazonq.featureDev.error.zipFileError'), { code: ZipFileError.errorName })
+ super(i18n('AWS.amazonq.featureDev.error.zipFileError'), { code: ZipFileError.name })
}
}
export class CodeIterationLimitError extends ToolkitError {
- static errorName = 'CodeIterationLimitError'
constructor() {
- super(i18n('AWS.amazonq.featureDev.error.codeIterationLimitError'), { code: CodeIterationLimitError.errorName })
+ super(i18n('AWS.amazonq.featureDev.error.codeIterationLimitError'), { code: CodeIterationLimitError.name })
}
}
export class MonthlyConversationLimitError extends ToolkitError {
- static errorName = 'MonthlyConversationLimitError'
constructor(message: string) {
- super(message, { code: MonthlyConversationLimitError.errorName })
+ super(message, { code: MonthlyConversationLimitError.name })
}
}
diff --git a/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts b/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts
index 6e57ed3c34f..9c159783625 100644
--- a/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts
+++ b/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts
@@ -14,13 +14,27 @@ import { Session } from '../../../../amazonqFeatureDev/session/session'
import { Prompter } from '../../../../shared/ui/prompter'
import { assertTelemetry, toFile } from '../../../testUtil'
import {
+ CodeIterationLimitError,
+ ContentLengthError,
+ createUserFacingErrorMessage,
+ FeatureDevServiceError,
+ MonthlyConversationLimitError,
NoChangeRequiredException,
+ PrepareRepoFailedError,
+ PromptRefusalException,
SelectedFolderNotInWorkspaceFolderError,
+ TabIdNotFoundError,
+ UploadCodeError,
+ UploadURLExpired,
+ UserMessageNotFoundError,
+ ZipFileError,
} from '../../../../amazonqFeatureDev/errors'
import { CodeGenState, PrepareCodeGenState } from '../../../../amazonqFeatureDev/session/sessionState'
import { FeatureDevClient } from '../../../../amazonqFeatureDev/client/featureDev'
import { createAmazonQUri } from '../../../../amazonq/commons/diff'
import { AuthUtil } from '../../../../codewhisperer'
+import { featureName, messageWithConversationId } from '../../../../amazonqFeatureDev'
+import { i18n } from '../../../../shared/i18n-helper'
let mockGetCodeGeneration: sinon.SinonStub
describe('Controller', () => {
@@ -209,7 +223,6 @@ describe('Controller', () => {
})
it('accepts valid source folders under a workspace root', async () => {
- const controllerSetup = await createController()
sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session)
sinon.stub(vscode.workspace, 'getWorkspaceFolder').returns(controllerSetup.workspaceFolder)
const expectedSourceRoot = path.join(controllerSetup.workspaceFolder.uri.fsPath, 'src')
@@ -385,22 +398,113 @@ describe('Controller', () => {
}
describe('processErrorChatMessage', function () {
- it('should handle NoChangeRequiredException', async function () {
- const noChangeRequiredException = new NoChangeRequiredException()
- sinon.stub(session, 'preloader').throws(noChangeRequiredException)
+ const runs = [
+ { name: 'ContentLengthError', error: new ContentLengthError() },
+ {
+ name: 'MonthlyConversationLimitError',
+ error: new MonthlyConversationLimitError('Service Quota Exceeded'),
+ },
+ {
+ name: 'FeatureDevServiceError',
+ error: new FeatureDevServiceError(
+ i18n('AWS.amazonq.featureDev.error.codeGen.default'),
+ 'GuardrailsException'
+ ),
+ },
+ { name: 'UploadCodeError', error: new UploadCodeError('403: Forbiden') },
+ { name: 'UserMessageNotFoundError', error: new UserMessageNotFoundError() },
+ { name: 'TabIdNotFoundError', error: new TabIdNotFoundError() },
+ { name: 'PrepareRepoFailedError', error: new PrepareRepoFailedError() },
+ { name: 'PromptRefusalException', error: new PromptRefusalException() },
+ { name: 'ZipFileError', error: new ZipFileError() },
+ { name: 'CodeIterationLimitError', error: new CodeIterationLimitError() },
+ { name: 'UploadURLExpired', error: new UploadURLExpired() },
+ { name: 'NoChangeRequiredException', error: new NoChangeRequiredException() },
+ { name: 'default', error: new Error() },
+ ]
+
+ function createTestErrorMessage(message: string) {
+ return createUserFacingErrorMessage(`${featureName} request failed: ${message}`)
+ }
+
+ async function verifyException(error: Error) {
+ sinon.stub(session, 'preloader').throws(error)
const sendAnswerSpy = sinon.stub(controllerSetup.messenger, 'sendAnswer')
+ const sendErrorMessageSpy = sinon.stub(controllerSetup.messenger, 'sendErrorMessage')
+ const sendMonthlyLimitErrorSpy = sinon.stub(controllerSetup.messenger, 'sendMonthlyLimitError')
await fireChatMessage()
- assert.strictEqual(
- sendAnswerSpy.calledWith({
- type: 'answer',
- tabID,
- message: noChangeRequiredException.message,
- canBeVoted: true,
- }),
- true
- )
+ switch (error.constructor.name) {
+ case ContentLengthError.name:
+ assert.ok(
+ sendAnswerSpy.calledWith({
+ type: 'answer',
+ tabID,
+ message: error.message + messageWithConversationId(session?.conversationIdUnsafe),
+ canBeVoted: true,
+ })
+ )
+ break
+ case MonthlyConversationLimitError.name:
+ assert.ok(sendMonthlyLimitErrorSpy.calledWith(tabID))
+ break
+ case FeatureDevServiceError.name:
+ case UploadCodeError.name:
+ case UserMessageNotFoundError.name:
+ case TabIdNotFoundError.name:
+ case PrepareRepoFailedError.name:
+ assert.ok(
+ sendErrorMessageSpy.calledWith(
+ createTestErrorMessage(error.message),
+ tabID,
+ session?.retries,
+ session?.conversationIdUnsafe
+ )
+ )
+ break
+ case PromptRefusalException.name:
+ case ZipFileError.name:
+ assert.ok(
+ sendErrorMessageSpy.calledWith(
+ createTestErrorMessage(error.message),
+ tabID,
+ 0,
+ session?.conversationIdUnsafe,
+ true
+ )
+ )
+ break
+ case NoChangeRequiredException.name:
+ case CodeIterationLimitError.name:
+ case UploadURLExpired.name:
+ assert.ok(
+ sendAnswerSpy.calledWith({
+ type: 'answer',
+ tabID,
+ message: error.message,
+ canBeVoted: true,
+ })
+ )
+ break
+ default:
+ assert.ok(
+ sendErrorMessageSpy.calledWith(
+ i18n('AWS.amazonq.featureDev.error.codeGen.default'),
+ tabID,
+ session?.retries,
+ session?.conversationIdUnsafe,
+ true
+ )
+ )
+ break
+ }
+ }
+
+ runs.forEach((run) => {
+ it(`should handle ${run.name}`, async function () {
+ await verifyException(run.error)
+ })
})
})
})
From 002d2c4cd75bfc97e8f6a7c7ee6c035ae73c8ef9 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 15 Oct 2024 13:31:34 -0700
Subject: [PATCH 48/87] deps: bump sass-loader from 12.6.0 to 16.0.2 #5784
---
package-lock.json | 27 +++++++++++----------------
packages/core/package.json | 2 +-
2 files changed, 12 insertions(+), 17 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 88196017184..918ca61de52 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13479,14 +13479,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/klona": {
- "version": "2.0.6",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 8"
- }
- },
"node_modules/koa": {
"version": "2.15.3",
"dev": true,
@@ -16311,29 +16303,29 @@
}
},
"node_modules/sass-loader": {
- "version": "12.6.0",
+ "version": "16.0.2",
+ "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.2.tgz",
+ "integrity": "sha512-Ll6iXZ1EYwYT19SqW4mSBb76vSSi8JgzElmzIerhEGgzB5hRjDQIWsPmuk1UrAXkR16KJHqVY0eH+5/uw9Tmfw==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "klona": "^2.0.4",
"neo-async": "^2.6.2"
},
"engines": {
- "node": ">= 12.13.0"
+ "node": ">= 18.12.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependencies": {
- "fibers": ">= 3.1.0",
- "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0",
+ "@rspack/core": "0.x || 1.x",
+ "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0",
"sass": "^1.3.0",
"sass-embedded": "*",
"webpack": "^5.0.0"
},
"peerDependenciesMeta": {
- "fibers": {
+ "@rspack/core": {
"optional": true
},
"node-sass": {
@@ -16344,6 +16336,9 @@
},
"sass-embedded": {
"optional": true
+ },
+ "webpack": {
+ "optional": true
}
}
},
@@ -19385,7 +19380,7 @@
"mocha-multi-reporters": "^1.5.1",
"readline-sync": "^1.4.9",
"sass": "^1.49.8",
- "sass-loader": "^12.6.0",
+ "sass-loader": "^16.0.2",
"sinon": "^14.0.0",
"style-loader": "^3.3.1",
"ts-node": "^10.9.1",
diff --git a/packages/core/package.json b/packages/core/package.json
index c549bbd9e53..490ce578f99 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -447,7 +447,7 @@
"mocha-multi-reporters": "^1.5.1",
"readline-sync": "^1.4.9",
"sass": "^1.49.8",
- "sass-loader": "^12.6.0",
+ "sass-loader": "^16.0.2",
"sinon": "^14.0.0",
"style-loader": "^3.3.1",
"ts-node": "^10.9.1",
From 00a6d292b3b3c67dbe7d5f708492cfe27e6ee3e1 Mon Sep 17 00:00:00 2001
From: Nikolas Komonen <118216176+nkomonen-amazon@users.noreply.github.com>
Date: Tue, 15 Oct 2024 16:36:58 -0400
Subject: [PATCH 49/87] fix(test): unreliable "AuthUtil CodeWhisperer uses
fallback connection" #5788
## Problem:
This test was flaky due to the event emitter and the test expecting 2 of
the same event to come in, but sometimes the second one was not capture.
This looks to be due to the second `captureEventOnce` call needing to do
some initial setup before it could capture an event. And I think there
was a race condition between it being setup in time and the event being
emitted before it could start listening.
## Solution:
Make a new function which does the event capture once and then listens N
amount of times. This is reliable.
---
.../test/unit/codewhisperer/util/authUtil.test.ts | 12 +++++-------
packages/core/src/test/testUtil.ts | 11 ++++++++++-
2 files changed, 15 insertions(+), 8 deletions(-)
diff --git a/packages/amazonq/test/unit/codewhisperer/util/authUtil.test.ts b/packages/amazonq/test/unit/codewhisperer/util/authUtil.test.ts
index f2f71c278d9..74a2c97dd75 100644
--- a/packages/amazonq/test/unit/codewhisperer/util/authUtil.test.ts
+++ b/packages/amazonq/test/unit/codewhisperer/util/authUtil.test.ts
@@ -13,12 +13,12 @@ import {
} from 'aws-core-vscode/codewhisperer'
import {
assertTelemetry,
- captureEventOnce,
getTestWindow,
SeverityLevel,
createBuilderIdProfile,
createSsoProfile,
createTestAuth,
+ captureEventNTimes,
} from 'aws-core-vscode/test'
import { Auth, Connection, isAnySsoConnection, isBuilderIdConnection } from 'aws-core-vscode/auth'
import { globals, vscodeComponent } from 'aws-core-vscode/shared'
@@ -231,15 +231,13 @@ describe('AuthUtil', async function () {
assert.strictEqual(auth.activeConnection?.id, authUtil.conn?.id)
// Switch to unsupported connection
- const cwAuthUpdatedConnection = captureEventOnce(authUtil.secondaryAuth.onDidChangeActiveConnection)
+ const cwAuthUpdatedConnection = captureEventNTimes(authUtil.secondaryAuth.onDidChangeActiveConnection, 2)
await auth.useConnection(unsupportedConn)
- // This is triggered when the main Auth connection is switched
+ // - This is triggered when the main Auth connection is switched
+ // - This is triggered by registerAuthListener() when it saves the previous active connection as a fallback.
await cwAuthUpdatedConnection
- // This is triggered by registerAuthListener() when it saves the previous active connection as a fallback.
- // TODO in a refactor see if we can simplify multiple multiple triggers on the same event.
- await captureEventOnce(authUtil.secondaryAuth.onDidChangeActiveConnection)
- // Is using the fallback connection
+ // TODO in a refactor see if we can simplify multiple multiple triggers on the same event.
assert.ok(authUtil.isConnected())
assert.ok(authUtil.isUsingSavedConnection)
assert.notStrictEqual(auth.activeConnection?.id, authUtil.conn?.id)
diff --git a/packages/core/src/test/testUtil.ts b/packages/core/src/test/testUtil.ts
index 138b802ba7b..26b0c473403 100644
--- a/packages/core/src/test/testUtil.ts
+++ b/packages/core/src/test/testUtil.ts
@@ -565,9 +565,18 @@ export function captureEvent(event: vscode.Event): EventCapturer {
* Captures the first value emitted by an event, optionally with a timeout
*/
export function captureEventOnce(event: vscode.Event, timeout?: number): Promise {
+ return captureEventNTimes(event, 1, timeout)
+}
+
+export function captureEventNTimes(event: vscode.Event, amount: number, timeout?: number): Promise {
return new Promise((resolve, reject) => {
const stop = () => reject(new Error('Timed out waiting for event'))
- event((data) => resolve(data))
+ let count = 0
+ event((data) => {
+ if (++count === amount) {
+ resolve(data)
+ }
+ })
if (timeout !== undefined) {
setTimeout(stop, timeout)
From bc52d137268eae305742b8e5dd19a5a61681a788 Mon Sep 17 00:00:00 2001
From: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com>
Date: Tue, 15 Oct 2024 14:31:53 -0700
Subject: [PATCH 50/87] telemetry(amazonq): cwsprChatProgrammingLanguage on
insertAtCursor, CopyAtClipboard #5768
## Problem
- Client side telemetry event `interactWithMessage` is missing
`cwsprChatProgrammingLanguage` parameter which helps to understand the
language of generated code.
## Solution
- Added `cwsprChatProgrammingLanguage` parameter for insertAtCursor and
CopyAtClipboard events.
- No Test cases were added in this PR.
- Added `extractCodeBlockLanguage` function as current UTG Sync API
sends python and java in the first chunk of generated code but thats the
not case for other languages. To counter this, I am checking entire
response for generated code language and emitting those metrics.
- Added `cwsprChatProgrammingLanguage` in
[commons](https://github.com/aws/aws-toolkit-common) package:
https://github.com/aws/aws-toolkit-common/pull/883
- Added `cwsprChatHasProjectContext and cwsprChatTotalCodeBlocks` in in
[commons](https://github.com/aws/aws-toolkit-common) package:
https://github.com/aws/aws-toolkit-common/pull/884
---
package-lock.json | 11 ++--
package.json | 2 +-
.../webview/ui/apps/cwChatConnector.ts | 10 +++-
.../core/src/amazonq/webview/ui/connector.ts | 13 +++--
packages/core/src/amazonq/webview/ui/main.ts | 16 ++++--
.../controllers/chat/messenger/messenger.ts | 12 +++-
.../controllers/chat/model.ts | 2 +
.../controllers/chat/telemetryHelper.ts | 2 +
.../view/connector/connector.ts | 3 +
.../view/messages/messageListener.ts | 2 +
packages/core/src/shared/markdown.ts | 22 ++++++++
.../src/shared/telemetry/vscodeTelemetry.json | 56 -------------------
.../core/src/test/shared/markdown.test.ts | 33 +++++++++++
13 files changed, 110 insertions(+), 74 deletions(-)
create mode 100644 packages/core/src/shared/markdown.ts
create mode 100644 packages/core/src/test/shared/markdown.test.ts
diff --git a/package-lock.json b/package-lock.json
index 918ca61de52..18ddaa6d1e5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,7 +19,7 @@
"vscode-nls-dev": "^4.0.4"
},
"devDependencies": {
- "@aws-toolkits/telemetry": "^1.0.267",
+ "@aws-toolkits/telemetry": "^1.0.272",
"@playwright/browser-chromium": "^1.43.1",
"@types/vscode": "^1.68.0",
"@types/vscode-webview": "^1.57.1",
@@ -5192,10 +5192,11 @@
}
},
"node_modules/@aws-toolkits/telemetry": {
- "version": "1.0.267",
- "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.267.tgz",
- "integrity": "sha512-qVEHuEW6WgqUafJP5oVtlaaWDtn2+6CklzqQgruqH7gxlNLBgi9pM9dpEC8xOYrHN3m1UW0LagUUgRS4ndDOyw==",
+ "version": "1.0.272",
+ "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.272.tgz",
+ "integrity": "sha512-cWmyTkiNDcDXaRjX7WdWO5FiNzXUKcrKpkYkkPgi8t+I9ZwwWwt6FKps7FjEGvFzTFx6I9XbsWM1mRrkuvxdQw==",
"dev": true,
+ "license": "Apache-2.0",
"dependencies": {
"ajv": "^6.12.6",
"fs-extra": "^11.1.0",
@@ -19392,7 +19393,7 @@
},
"engines": {
"npm": "^10.1.0",
- "vscode": "^1.83.0"
+ "vscode": "^1.68.0"
}
},
"packages/core/node_modules/@types/node": {
diff --git a/package.json b/package.json
index 8d82cac692f..e4e5437a706 100644
--- a/package.json
+++ b/package.json
@@ -39,7 +39,7 @@
"generateNonCodeFiles": "npm run generateNonCodeFiles -w packages/ --if-present"
},
"devDependencies": {
- "@aws-toolkits/telemetry": "^1.0.267",
+ "@aws-toolkits/telemetry": "^1.0.272",
"@playwright/browser-chromium": "^1.43.1",
"@types/vscode": "^1.68.0",
"@types/vscode-webview": "^1.57.1",
diff --git a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts
index 09963fb77ab..3e5ea73b3ba 100644
--- a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts
+++ b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts
@@ -111,7 +111,8 @@ export class Connector {
eventId?: string,
codeBlockIndex?: number,
totalCodeBlocks?: number,
- userIntent?: string
+ userIntent?: string,
+ codeBlockLanguage?: string
): void => {
this.sendMessageToExtension({
tabID: tabID,
@@ -125,6 +126,7 @@ export class Connector {
codeBlockIndex,
totalCodeBlocks,
userIntent,
+ codeBlockLanguage,
})
}
@@ -137,7 +139,8 @@ export class Connector {
eventId?: string,
codeBlockIndex?: number,
totalCodeBlocks?: number,
- userIntent?: string
+ userIntent?: string,
+ codeBlockLanguage?: string
): void => {
this.sendMessageToExtension({
tabID: tabID,
@@ -151,6 +154,7 @@ export class Connector {
codeBlockIndex,
totalCodeBlocks,
userIntent,
+ codeBlockLanguage,
})
}
@@ -295,6 +299,7 @@ export class Connector {
canBeVoted: true,
codeReference: messageData.codeReference,
userIntent: messageData.userIntent,
+ codeBlockLanguage: messageData.codeBlockLanguage,
}
// If it is not there we will not set it
@@ -328,6 +333,7 @@ export class Connector {
messageId: messageData.messageID,
codeReference: messageData.codeReference,
userIntent: messageData.userIntent,
+ codeBlockLanguage: messageData.codeBlockLanguage,
followUp:
messageData.followUps !== undefined && messageData.followUps.length > 0
? {
diff --git a/packages/core/src/amazonq/webview/ui/connector.ts b/packages/core/src/amazonq/webview/ui/connector.ts
index 0ec1ed5a4fd..0f4aa82652c 100644
--- a/packages/core/src/amazonq/webview/ui/connector.ts
+++ b/packages/core/src/amazonq/webview/ui/connector.ts
@@ -51,6 +51,7 @@ export interface ChatPayload {
export interface CWCChatItem extends ChatItem {
traceId?: string
userIntent?: UserIntent
+ codeBlockLanguage?: string
}
export interface ConnectorProps {
@@ -248,7 +249,8 @@ export class Connector {
eventId?: string,
codeBlockIndex?: number,
totalCodeBlocks?: number,
- userIntent?: string
+ userIntent?: string,
+ codeBlockLanguage?: string
): void => {
switch (this.tabsStorage.getTab(tabID)?.type) {
case 'cwc':
@@ -261,7 +263,8 @@ export class Connector {
eventId,
codeBlockIndex,
totalCodeBlocks,
- userIntent
+ userIntent,
+ codeBlockLanguage
)
break
case 'featuredev':
@@ -337,7 +340,8 @@ export class Connector {
eventId?: string,
codeBlockIndex?: number,
totalCodeBlocks?: number,
- userIntent?: string
+ userIntent?: string,
+ codeBlockLanguage?: string
): void => {
switch (this.tabsStorage.getTab(tabID)?.type) {
case 'cwc':
@@ -350,7 +354,8 @@ export class Connector {
eventId,
codeBlockIndex,
totalCodeBlocks,
- userIntent
+ userIntent,
+ codeBlockLanguage
)
break
case 'featuredev':
diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts
index 429c5b8bb84..84e76273a59 100644
--- a/packages/core/src/amazonq/webview/ui/main.ts
+++ b/packages/core/src/amazonq/webview/ui/main.ts
@@ -38,7 +38,7 @@ export const createMynahUI = (
// eslint-disable-next-line prefer-const
let connector: Connector
//Store the mapping between messageId and messageUserIntent for amazonq_interactWithMessage telemetry
- const messageUserIntentMap = new Map()
+ const responseMetadata = new Map()
window.addEventListener('error', (e) => {
const { error, message } = e
@@ -253,8 +253,12 @@ export const createMynahUI = (
? { type: ChatItemType.CODE_RESULT, fileList: item.fileList }
: {}),
})
- if (item.messageId !== undefined && item.userIntent !== undefined) {
- messageUserIntentMap.set(item.messageId, item.userIntent)
+ if (
+ item.messageId !== undefined &&
+ item.userIntent !== undefined &&
+ item.codeBlockLanguage !== undefined
+ ) {
+ responseMetadata.set(item.messageId, [item.userIntent, item.codeBlockLanguage])
}
ideApi.postMessage({
command: 'update-chat-message-telemetry',
@@ -524,7 +528,8 @@ export const createMynahUI = (
eventId,
codeBlockIndex,
totalCodeBlocks,
- messageUserIntentMap.get(messageId) ?? undefined
+ responseMetadata.get(messageId)?.[0] ?? undefined,
+ responseMetadata.get(messageId)?.[1] ?? undefined
)
},
onCodeBlockActionClicked: (
@@ -591,7 +596,8 @@ export const createMynahUI = (
eventId,
codeBlockIndex,
totalCodeBlocks,
- messageUserIntentMap.get(messageId) ?? undefined
+ responseMetadata.get(messageId)?.[0] ?? undefined,
+ responseMetadata.get(messageId)?.[1] ?? undefined
)
mynahUI.notify({
type: NotificationType.SUCCESS,
diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
index c16bbf3c24b..41a31d30edd 100644
--- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
+++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts
@@ -33,6 +33,7 @@ import { CodeScanIssue } from '../../../../codewhisperer/models/model'
import { marked } from 'marked'
import { JSDOM } from 'jsdom'
import { LspController } from '../../../../amazonq/lsp/lspController'
+import { extractCodeBlockLanguage } from '../../../../shared/markdown'
export type StaticTextResponseType = 'quick-action-help' | 'onboarding-help' | 'transform' | 'help'
@@ -87,6 +88,7 @@ export class Messenger {
triggerID,
messageID: '',
userIntent: undefined,
+ codeBlockLanguage: undefined,
},
tabID
)
@@ -131,6 +133,7 @@ export class Messenger {
let codeReference: CodeReference[] = []
let followUps: FollowUp[] = []
let relatedSuggestions: Suggestion[] = []
+ let codeBlockLanguage: string = 'plaintext'
if (response.generateAssistantResponseResponse === undefined) {
throw new ToolkitError(
@@ -182,7 +185,9 @@ export class Messenger {
chatEvent.assistantResponseEvent.content.length > 0
) {
message += chatEvent.assistantResponseEvent.content
-
+ if (codeBlockLanguage === 'plaintext') {
+ codeBlockLanguage = extractCodeBlockLanguage(message)
+ }
this.dispatcher.sendChatMessage(
new ChatMessage(
{
@@ -195,6 +200,7 @@ export class Messenger {
triggerID,
messageID,
userIntent: triggerPayload.userIntent,
+ codeBlockLanguage: codeBlockLanguage,
},
tabID
)
@@ -272,6 +278,7 @@ export class Messenger {
triggerID,
messageID,
userIntent: triggerPayload.userIntent,
+ codeBlockLanguage: codeBlockLanguage,
},
tabID
)
@@ -290,6 +297,7 @@ export class Messenger {
triggerID,
messageID,
userIntent: triggerPayload.userIntent,
+ codeBlockLanguage: undefined,
},
tabID
)
@@ -307,6 +315,7 @@ export class Messenger {
triggerID,
messageID,
userIntent: triggerPayload.userIntent,
+ codeBlockLanguage: undefined,
},
tabID
)
@@ -431,6 +440,7 @@ export class Messenger {
triggerID,
messageID: 'static_message_' + triggerID,
userIntent: undefined,
+ codeBlockLanguage: undefined,
},
tabID
)
diff --git a/packages/core/src/codewhispererChat/controllers/chat/model.ts b/packages/core/src/codewhispererChat/controllers/chat/model.ts
index d2f7e4ca627..4c269298fd9 100644
--- a/packages/core/src/codewhispererChat/controllers/chat/model.ts
+++ b/packages/core/src/codewhispererChat/controllers/chat/model.ts
@@ -46,6 +46,7 @@ export interface InsertCodeAtCursorPosition {
eventId: string
codeBlockIndex: number
totalCodeBlocks: number
+ codeBlockLanguage: string
}
export interface CopyCodeToClipboard {
@@ -59,6 +60,7 @@ export interface CopyCodeToClipboard {
eventId: string
codeBlockIndex: number
totalCodeBlocks: number
+ codeBlockLanguage: string
}
export interface AcceptDiff {
diff --git a/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts b/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts
index e5b49a6d59c..0522ab29cd6 100644
--- a/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts
+++ b/packages/core/src/codewhispererChat/controllers/chat/telemetryHelper.ts
@@ -203,6 +203,7 @@ export class CWCTelemetryHelper {
cwsprChatCodeBlockIndex: message.codeBlockIndex,
cwsprChatTotalCodeBlocks: message.totalCodeBlocks,
cwsprChatHasProjectContext: this.responseWithProjectContext.get(message.messageId),
+ cwsprChatProgrammingLanguage: message.codeBlockLanguage,
}
break
case 'code_was_copied_to_clipboard':
@@ -220,6 +221,7 @@ export class CWCTelemetryHelper {
cwsprChatCodeBlockIndex: message.codeBlockIndex,
cwsprChatTotalCodeBlocks: message.totalCodeBlocks,
cwsprChatHasProjectContext: this.responseWithProjectContext.get(message.messageId),
+ cwsprChatProgrammingLanguage: message.codeBlockLanguage,
}
break
case 'accept_diff':
diff --git a/packages/core/src/codewhispererChat/view/connector/connector.ts b/packages/core/src/codewhispererChat/view/connector/connector.ts
index 00a8217de49..02794af5fb3 100644
--- a/packages/core/src/codewhispererChat/view/connector/connector.ts
+++ b/packages/core/src/codewhispererChat/view/connector/connector.ts
@@ -142,6 +142,7 @@ export interface ChatMessageProps {
readonly triggerID: string
readonly messageID: string
readonly userIntent: string | undefined
+ readonly codeBlockLanguage: string | undefined
}
export class ChatMessage extends UiMessage {
@@ -155,6 +156,7 @@ export class ChatMessage extends UiMessage {
readonly triggerID: string
readonly messageID: string | undefined
readonly userIntent: string | undefined
+ readonly codeBlockLanguage: string | undefined
override type = 'chatMessage'
constructor(props: ChatMessageProps, tabID: string) {
@@ -168,6 +170,7 @@ export class ChatMessage extends UiMessage {
this.triggerID = props.triggerID
this.messageID = props.messageID
this.userIntent = props.userIntent
+ this.codeBlockLanguage = props.codeBlockLanguage
}
}
diff --git a/packages/core/src/codewhispererChat/view/messages/messageListener.ts b/packages/core/src/codewhispererChat/view/messages/messageListener.ts
index abe37c019fd..93c750ab01b 100644
--- a/packages/core/src/codewhispererChat/view/messages/messageListener.ts
+++ b/packages/core/src/codewhispererChat/view/messages/messageListener.ts
@@ -165,6 +165,7 @@ export class UIMessageListener {
eventId: msg.eventId,
codeBlockIndex: msg.codeBlockIndex,
totalCodeBlocks: msg.totalCodeBlocks,
+ codeBlockLanguage: msg.codeBlockLanguage,
})
}
@@ -196,6 +197,7 @@ export class UIMessageListener {
eventId: msg.eventId,
codeBlockIndex: msg.codeBlockIndex,
totalCodeBlocks: msg.totalCodeBlocks,
+ codeBlockLanguage: msg.codeBlockLanguage,
})
}
diff --git a/packages/core/src/shared/markdown.ts b/packages/core/src/shared/markdown.ts
new file mode 100644
index 00000000000..215e6603906
--- /dev/null
+++ b/packages/core/src/shared/markdown.ts
@@ -0,0 +1,22 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export const extractCodeBlockLanguage = (message: string) => {
+ // This fulfills both the cases of unit test generation(java, python) and general use case(Non java and Non python) languages.
+ const codeBlockStart = message.indexOf('```')
+ if (codeBlockStart === -1) {
+ return 'plaintext'
+ }
+
+ const languageStart = codeBlockStart + 3
+ const languageEnd = message.indexOf('\n', languageStart)
+
+ if (languageEnd === -1) {
+ return 'plaintext'
+ }
+
+ const language = message.substring(languageStart, languageEnd).trim()
+ return language !== '' ? language : 'plaintext'
+}
diff --git a/packages/core/src/shared/telemetry/vscodeTelemetry.json b/packages/core/src/shared/telemetry/vscodeTelemetry.json
index 36a4b110583..a82c949e06c 100644
--- a/packages/core/src/shared/telemetry/vscodeTelemetry.json
+++ b/packages/core/src/shared/telemetry/vscodeTelemetry.json
@@ -103,11 +103,6 @@
"type": "int",
"description": "CPU used by LSP server as a percentage of all available CPUs on the system"
},
- {
- "name": "cwsprChatProgrammingLanguage",
- "type": "string",
- "description": "Programming language associated with the message"
- },
{
"name": "cwsprChatConversationType",
"type": "string",
@@ -841,57 +836,6 @@
}
]
},
- {
- "name": "amazonq_interactWithMessage",
- "description": "When a user interacts with a message in the conversation",
- "metadata": [
- {
- "type": "cwsprChatConversationId"
- },
- {
- "type": "credentialStartUrl",
- "required": false
- },
- {
- "type": "cwsprChatMessageId"
- },
- {
- "type": "cwsprChatUserIntent",
- "required": false
- },
- {
- "type": "cwsprChatInteractionType"
- },
- {
- "type": "cwsprChatInteractionTarget",
- "required": false
- },
- {
- "type": "cwsprChatAcceptedCharactersLength",
- "required": false
- },
- {
- "type": "cwsprChatAcceptedNumberOfLines",
- "required": false
- },
- {
- "type": "cwsprChatHasReference",
- "required": false
- },
- {
- "type": "cwsprChatCodeBlockIndex",
- "required": false
- },
- {
- "type": "cwsprChatTotalCodeBlocks",
- "required": false
- },
- {
- "type": "cwsprChatHasProjectContext",
- "required": false
- }
- ]
- },
{
"name": "amazonq_modifyCode",
"description": "% of code modified by the user after copying/inserting code from a message",
diff --git a/packages/core/src/test/shared/markdown.test.ts b/packages/core/src/test/shared/markdown.test.ts
new file mode 100644
index 00000000000..f362a476925
--- /dev/null
+++ b/packages/core/src/test/shared/markdown.test.ts
@@ -0,0 +1,33 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert'
+import { extractCodeBlockLanguage } from '../../shared/markdown'
+
+describe('extractCodeBlockLanguage', () => {
+ it('should return "plaintext" when no code block is present', () => {
+ const message = 'This is a message without a code block'
+ assert.strictEqual(extractCodeBlockLanguage(message), 'plaintext')
+ })
+
+ it('should return the language when a code block with language is present', () => {
+ const message = 'Here is some code:\n```javascript\nconsole.log("Hello");\n```'
+ assert.strictEqual(extractCodeBlockLanguage(message), 'javascript')
+ })
+
+ it('should return "plaintext" when a code block is present but no language is specified', () => {
+ const message = 'Here is some code:\n```\nconsole.log("Hello");\n```'
+ assert.strictEqual(extractCodeBlockLanguage(message), 'plaintext')
+ })
+
+ it('should handle whitespace before the language specification', () => {
+ const message = 'Code:\n``` typescript\nconst x: number = 5;\n```'
+ assert.strictEqual(extractCodeBlockLanguage(message), 'typescript')
+ })
+
+ it('should handle empty messages', () => {
+ assert.strictEqual(extractCodeBlockLanguage(''), 'plaintext')
+ })
+})
From 360cb281c66c8193fe597f8879e677587866c3d8 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 15 Oct 2024 15:32:49 -0700
Subject: [PATCH 51/87] deps: bump @types/webpack-env from 1.18.1 to 1.18.5
(#5606)
Bumps
[@types/webpack-env](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/webpack-env)
from 1.18.1 to 1.18.5.
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 9 +++++----
package.json | 2 +-
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 18ddaa6d1e5..93e117524fb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,7 +23,7 @@
"@playwright/browser-chromium": "^1.43.1",
"@types/vscode": "^1.68.0",
"@types/vscode-webview": "^1.57.1",
- "@types/webpack-env": "^1.18.1",
+ "@types/webpack-env": "^1.18.5",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.14.1",
"@vscode/codicons": "^0.0.33",
@@ -7219,9 +7219,10 @@
"license": "MIT"
},
"node_modules/@types/webpack-env": {
- "version": "1.18.1",
- "dev": true,
- "license": "MIT"
+ "version": "1.18.5",
+ "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.5.tgz",
+ "integrity": "sha512-wz7kjjRRj8/Lty4B+Kr0LN6Ypc/3SymeCCGSbaXp2leH0ZVg/PriNiOwNj4bD4uphI7A8NXS4b6Gl373sfO5mA==",
+ "dev": true
},
"node_modules/@types/whatwg-url": {
"version": "11.0.4",
diff --git a/package.json b/package.json
index e4e5437a706..869e7e5fe70 100644
--- a/package.json
+++ b/package.json
@@ -43,7 +43,7 @@
"@playwright/browser-chromium": "^1.43.1",
"@types/vscode": "^1.68.0",
"@types/vscode-webview": "^1.57.1",
- "@types/webpack-env": "^1.18.1",
+ "@types/webpack-env": "^1.18.5",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.14.1",
"@vscode/codicons": "^0.0.33",
From aa92cc4f8303989f4829363163163deb97de0861 Mon Sep 17 00:00:00 2001
From: Roger Zhang
Date: Wed, 16 Oct 2024 10:53:35 -0700
Subject: [PATCH 52/87] test(credentials): do not modify developer $HOME #5791
## Problem
https://github.com/aws/aws-toolkit-vscode/blob/0164d4145e58ae036ddf3815455ea12a159d491d/packages/core/src/test/shared/credentials/userCredentialsUtils.test.ts#L38-L39
This test is deleting my actual aws config file.
## Solution
Update the environment variable to point to a fake location before test
Then recover them after test
---
.../credentials/userCredentialsUtils.test.ts | 15 ++-------------
... Fix-a0acdaa1-7356-4f58-bc9b-fb7b708fe831.json | 4 ++++
2 files changed, 6 insertions(+), 13 deletions(-)
create mode 100644 packages/toolkit/.changes/next-release/Bug Fix-a0acdaa1-7356-4f58-bc9b-fb7b708fe831.json
diff --git a/packages/core/src/test/shared/credentials/userCredentialsUtils.test.ts b/packages/core/src/test/shared/credentials/userCredentialsUtils.test.ts
index ddbd250c2cd..eaeb9bc571d 100644
--- a/packages/core/src/test/shared/credentials/userCredentialsUtils.test.ts
+++ b/packages/core/src/test/shared/credentials/userCredentialsUtils.test.ts
@@ -18,31 +18,20 @@ import {
import { UserCredentialsUtils } from '../../../shared/credentials/userCredentialsUtils'
import { EnvironmentVariables } from '../../../shared/environmentVariables'
import { makeTemporaryToolkitFolder } from '../../../shared/filesystemUtilities'
-import { getConfigFilename, getCredentialsFilename } from '../../../auth/credentials/sharedCredentialsFile'
import { fs } from '../../../shared'
describe('UserCredentialsUtils', function () {
let tempFolder: string
- let defaultConfigFileName: string
- let defaultCredentialsFilename: string
- before(async function () {
- defaultConfigFileName = getConfigFilename()
- defaultCredentialsFilename = getCredentialsFilename()
+ beforeEach(async function () {
// Make a temp folder for all these tests
// Stick some temp credentials files in there to load from
tempFolder = await makeTemporaryToolkitFolder()
})
afterEach(async function () {
- await fs.delete(defaultConfigFileName, { recursive: true })
- await fs.delete(defaultCredentialsFilename, { recursive: true })
-
- sinon.restore()
- })
-
- after(async function () {
await fs.delete(tempFolder, { recursive: true })
+ sinon.restore()
})
describe('findExistingCredentialsFilenames', function () {
diff --git a/packages/toolkit/.changes/next-release/Bug Fix-a0acdaa1-7356-4f58-bc9b-fb7b708fe831.json b/packages/toolkit/.changes/next-release/Bug Fix-a0acdaa1-7356-4f58-bc9b-fb7b708fe831.json
new file mode 100644
index 00000000000..ecefcc6e87b
--- /dev/null
+++ b/packages/toolkit/.changes/next-release/Bug Fix-a0acdaa1-7356-4f58-bc9b-fb7b708fe831.json
@@ -0,0 +1,4 @@
+{
+ "type": "Bug Fix",
+ "description": "Fix userCredentialsUtils.test.ts so it won't remove the actual aws config"
+}
From 59ba5f919b2f641d72fe9a5a232ac6d1d46b7de2 Mon Sep 17 00:00:00 2001
From: Hweinstock <42325418+Hweinstock@users.noreply.github.com>
Date: Wed, 16 Oct 2024 17:02:53 -0400
Subject: [PATCH 53/87] docs(tests): mention performance tests in TESTPLAN.md
#5793
---
docs/TESTPLAN.md | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/docs/TESTPLAN.md b/docs/TESTPLAN.md
index 0323ce3165f..56a299c9a5b 100644
--- a/docs/TESTPLAN.md
+++ b/docs/TESTPLAN.md
@@ -42,6 +42,11 @@ The test suite has the following categories of tests:
- Live in `src/testE2E`
- These tests are heavier than Integration tests.
- Some E2E tests have a more complicated architecture, described in [TEST_E2E](./TEST_E2E.md)
+- Performance Tests: **slow** tests
+ - Live in `src/testInteg/perf`.
+ - A subset of integration tests focused on catching performance regressions.
+ - Use a combination of operation counting and system usage statistics such as cpu usage, memory usage, and duration.
+ - Each test is often repeated 10 or more times for less variant system usage statistics, then median of runs is used.
## Test files
From ddfc64b634e61555a3c90df86c529e3131a832cb Mon Sep 17 00:00:00 2001
From: Hweinstock <42325418+Hweinstock@users.noreply.github.com>
Date: Wed, 16 Oct 2024 17:31:11 -0400
Subject: [PATCH 54/87] refactor(fs): use node:fs, drop fs-extra in
copyFiles.ts #5761
## Problem
Eliminate fs-extra from the codebase.
## Solution
We can use `fs.cp` since node v16.7.0+.
Note that `overwrite` is now `force` based on
https://nodejs.org/api/fs.html#fscpsyncsrc-dest-options
Bump the dependency @types/node to avoid casting (fs as any).
---
buildspec/shared/linux-pre_build.sh | 2 +-
package-lock.json | 18 ++++++++++---
package.json | 1 +
packages/amazonq/scripts/build/copyFiles.ts | 26 ++++++++++---------
.../e2e/amazonq/framework/jsdomInjector.ts | 2 +-
packages/core/scripts/build/copyFiles.ts | 17 ++++++------
packages/toolkit/scripts/build/copyFiles.ts | 17 ++++++------
7 files changed, 50 insertions(+), 33 deletions(-)
diff --git a/buildspec/shared/linux-pre_build.sh b/buildspec/shared/linux-pre_build.sh
index 25e9f0c879f..102103ff30c 100644
--- a/buildspec/shared/linux-pre_build.sh
+++ b/buildspec/shared/linux-pre_build.sh
@@ -22,5 +22,5 @@ if [ "$TOOLKITS_CODEARTIFACT_DOMAIN" ] && [ "$TOOLKITS_CODEARTIFACT_REPO" ] && [
fi
# TODO: move this to the "install" phase?
-export NODE_OPTIONS=--max-old-space-size=8192
+export NODE_OPTIONS='--max-old-space-size=8192'
npm 2>&1 ci | run_and_report 2 'npm WARN deprecated' 'Deprecated dependencies must be updated.'
diff --git a/package-lock.json b/package-lock.json
index 93e117524fb..404c95a936e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"plugins/*"
],
"dependencies": {
+ "@types/node": "^22.7.5",
"vscode-nls": "^5.2.0",
"vscode-nls-dev": "^4.0.4"
},
@@ -7058,8 +7059,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "14.18.63",
- "license": "MIT"
+ "version": "22.7.5",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
+ "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.19.2"
+ }
},
"node_modules/@types/node-fetch": {
"version": "2.6.9",
@@ -17778,6 +17784,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/undici-types": {
+ "version": "6.19.8",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
+ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
+ "license": "MIT"
+ },
"node_modules/unescape-html": {
"version": "1.1.0",
"license": "MIT"
@@ -19394,7 +19406,7 @@
},
"engines": {
"npm": "^10.1.0",
- "vscode": "^1.68.0"
+ "vscode": "^1.83.0"
}
},
"packages/core/node_modules/@types/node": {
diff --git a/package.json b/package.json
index 869e7e5fe70..8de4db99c14 100644
--- a/package.json
+++ b/package.json
@@ -69,6 +69,7 @@
"webpack-merge": "^5.10.0"
},
"dependencies": {
+ "@types/node": "^22.7.5",
"vscode-nls": "^5.2.0",
"vscode-nls-dev": "^4.0.4"
}
diff --git a/packages/amazonq/scripts/build/copyFiles.ts b/packages/amazonq/scripts/build/copyFiles.ts
index c1b0861321d..725c66ad7c0 100644
--- a/packages/amazonq/scripts/build/copyFiles.ts
+++ b/packages/amazonq/scripts/build/copyFiles.ts
@@ -4,7 +4,7 @@
*/
/* eslint-disable no-restricted-imports */
-import * as fs from 'fs-extra'
+import fs from 'fs'
import * as path from 'path'
// Moves all dependencies into `dist`
@@ -73,14 +73,14 @@ const tasks: CopyTask[] = [
},
]
-async function copy(task: CopyTask): Promise {
+function copy(task: CopyTask): void {
const src = path.resolve(projectRoot, task.target)
const dst = path.resolve(outRoot, task.destination ?? task.target)
try {
- await fs.copy(src, dst, {
+ fs.cpSync(src, dst, {
recursive: true,
- overwrite: true,
+ force: true,
errorOnExist: false,
})
} catch (error) {
@@ -88,18 +88,20 @@ async function copy(task: CopyTask): Promise {
}
}
-void (async () => {
- const args = process.argv.slice(2)
- if (args.includes('--vueHr')) {
- vueHr = true
- console.log('Using Vue Hot Reload webpacks from core/')
- }
+const args = process.argv.slice(2)
+if (args.includes('--vueHr')) {
+ vueHr = true
+ console.log('Using Vue Hot Reload webpacks from core/')
+}
+function main() {
try {
- await Promise.all(tasks.map(copy))
+ tasks.map(copy)
} catch (error) {
console.error('`copyFiles.ts` failed')
console.error(error)
process.exit(1)
}
-})()
+}
+
+void main()
diff --git a/packages/amazonq/test/e2e/amazonq/framework/jsdomInjector.ts b/packages/amazonq/test/e2e/amazonq/framework/jsdomInjector.ts
index ef92521b1d9..f5d2d1c2770 100644
--- a/packages/amazonq/test/e2e/amazonq/framework/jsdomInjector.ts
+++ b/packages/amazonq/test/e2e/amazonq/framework/jsdomInjector.ts
@@ -34,5 +34,5 @@ export function injectJSDOM() {
})
// jsdom doesn't have support for structuredClone. See https://github.com/jsdom/jsdom/issues/3363
- global.structuredClone = (val) => JSON.parse(JSON.stringify(val))
+ global.structuredClone = (val: any) => JSON.parse(JSON.stringify(val))
}
diff --git a/packages/core/scripts/build/copyFiles.ts b/packages/core/scripts/build/copyFiles.ts
index ee5c9492bcb..e926a6d9963 100644
--- a/packages/core/scripts/build/copyFiles.ts
+++ b/packages/core/scripts/build/copyFiles.ts
@@ -4,7 +4,7 @@
*/
/* eslint-disable no-restricted-imports */
-import * as fs from 'fs-extra'
+import fs from 'fs'
import * as path from 'path'
// Moves all dependencies into `dist`
@@ -46,27 +46,28 @@ const tasks: CopyTask[] = [
},
]
-async function copy(task: CopyTask): Promise {
+function copy(task: CopyTask): void {
const src = path.resolve(projectRoot, task.target)
const dst = path.resolve(outRoot, task.destination ?? task.target)
try {
- await fs.copy(src, dst, {
+ fs.cpSync(src, dst, {
recursive: true,
- overwrite: true,
+ force: true,
errorOnExist: false,
})
} catch (error) {
throw new Error(`Copy "${src}" to "${dst}" failed: ${error instanceof Error ? error.message : error}`)
}
}
-
-void (async () => {
+function main() {
try {
- await Promise.all(tasks.map(copy))
+ tasks.map(copy)
} catch (error) {
console.error('`copyFiles.ts` failed')
console.error(error)
process.exit(1)
}
-})()
+}
+
+void main()
diff --git a/packages/toolkit/scripts/build/copyFiles.ts b/packages/toolkit/scripts/build/copyFiles.ts
index 99c79124637..6cea899b4ca 100644
--- a/packages/toolkit/scripts/build/copyFiles.ts
+++ b/packages/toolkit/scripts/build/copyFiles.ts
@@ -4,7 +4,7 @@
*/
/* eslint-disable no-restricted-imports */
-import * as fs from 'fs-extra'
+import fs from 'fs'
import * as path from 'path'
// Copies various dependencies into "dist/".
@@ -100,27 +100,28 @@ const tasks: CopyTask[] = [
},
]
-async function copy(task: CopyTask): Promise {
+function copy(task: CopyTask): void {
const src = path.resolve(projectRoot, task.target)
const dst = path.resolve(outRoot, task.destination ?? task.target)
try {
- await fs.copy(src, dst, {
+ fs.cpSync(src, dst, {
recursive: true,
- overwrite: true,
+ force: true,
errorOnExist: false,
})
} catch (error) {
throw new Error(`Copy "${src}" to "${dst}" failed: ${error instanceof Error ? error.message : error}`)
}
}
-
-void (async () => {
+function main() {
try {
- await Promise.all(tasks.map(copy))
+ tasks.map(copy)
} catch (error) {
console.error('`copyFiles.ts` failed')
console.error(error)
process.exit(1)
}
-})()
+}
+
+void main()
From 7def140b27fb0b48e5da2bf21ea25aebf77abf6a Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 16 Oct 2024 14:37:50 -0700
Subject: [PATCH 55/87] deps: bump @types/readline-sync from 1.4.4 to 1.4.8
(#5795)
---
package-lock.json | 9 +++++----
packages/core/package.json | 2 +-
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 404c95a936e..dd79dd392cd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7119,9 +7119,10 @@
}
},
"node_modules/@types/readline-sync": {
- "version": "1.4.4",
- "dev": true,
- "license": "MIT"
+ "version": "1.4.8",
+ "resolved": "https://registry.npmjs.org/@types/readline-sync/-/readline-sync-1.4.8.tgz",
+ "integrity": "sha512-BL7xOf0yKLA6baAX6MMOnYkoflUyj/c7y3pqMRfU0va7XlwHAOTOIo4x55P/qLfMsuaYdJJKubToLqRVmRtRZA==",
+ "dev": true
},
"node_modules/@types/responselike": {
"version": "1.0.0",
@@ -19370,7 +19371,7 @@
"@types/node-fetch": "^2.6.8",
"@types/prismjs": "^1.26.0",
"@types/proper-lockfile": "^4.1.4",
- "@types/readline-sync": "^1.4.3",
+ "@types/readline-sync": "^1.4.8",
"@types/semver": "^7.5.0",
"@types/sinon": "^10.0.5",
"@types/sinonjs__fake-timers": "^8.1.2",
diff --git a/packages/core/package.json b/packages/core/package.json
index 490ce578f99..be6f09e5cc8 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -423,7 +423,7 @@
"@types/node-fetch": "^2.6.8",
"@types/prismjs": "^1.26.0",
"@types/proper-lockfile": "^4.1.4",
- "@types/readline-sync": "^1.4.3",
+ "@types/readline-sync": "^1.4.8",
"@types/semver": "^7.5.0",
"@types/sinon": "^10.0.5",
"@types/sinonjs__fake-timers": "^8.1.2",
From 2944c4b19d045c9f68230260538e740ecd90ba9f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 16 Oct 2024 14:56:11 -0700
Subject: [PATCH 56/87] deps: bump webpack from 5.94.0 to 5.95.0 (#5794)
---
package-lock.json | 7 ++++---
package.json | 2 +-
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index dd79dd392cd..a725e954750 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -44,7 +44,7 @@
"pretty-quick": "^4.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.0.4",
- "webpack": "^5.83.0",
+ "webpack": "^5.95.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1",
"webpack-merge": "^5.10.0"
@@ -18317,9 +18317,10 @@
}
},
"node_modules/webpack": {
- "version": "5.94.0",
+ "version": "5.95.0",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz",
+ "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@types/estree": "^1.0.5",
"@webassemblyjs/ast": "^1.12.1",
diff --git a/package.json b/package.json
index 8de4db99c14..3e8821b017c 100644
--- a/package.json
+++ b/package.json
@@ -63,7 +63,7 @@
"pretty-quick": "^4.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.0.4",
- "webpack": "^5.83.0",
+ "webpack": "^5.95.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1",
"webpack-merge": "^5.10.0"
From b08ef6e9315e1337ccf2478692358665e48660e5 Mon Sep 17 00:00:00 2001
From: Nikolas Komonen <118216176+nkomonen-amazon@users.noreply.github.com>
Date: Wed, 16 Oct 2024 23:33:10 -0400
Subject: [PATCH 57/87] fix(crash): handle sleep/wake appropriately (#5787)
## Problem:
When a user does a sleep then wake of their computer, the heartbeat is
not up to date since it cannot be sent when the user's computer is
asleep.
Due to this there is a race condition on wake between the next fresh
heartbeat being sent versus when we check the heartbeats to determine if
they are stale (crash). If the crash checker runs before a new heartbeat
can be sent, it will be seen as a crash.
## Solution:
Use a TimeLag class that helps to determine when there is a time
discrepancy. It works by updating a state every second, and if we
determine that the next update to that state took longer than a second,
we determine that there was a lag. Then we simply skip the next crash
check, allowing a fresh heartbeat to be sent.
---
License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---------
Signed-off-by: nkomonen-amazon
---
packages/core/src/shared/constants.ts | 6 +-
packages/core/src/shared/crashMonitoring.ts | 224 +++++++++---------
packages/core/src/shared/errors.ts | 6 +-
.../core/src/shared/utilities/timeoutUtils.ts | 67 ++++++
.../src/test/shared/crashMonitoring.test.ts | 3 +-
5 files changed, 180 insertions(+), 126 deletions(-)
diff --git a/packages/core/src/shared/constants.ts b/packages/core/src/shared/constants.ts
index 98e61d220f0..908624383dc 100644
--- a/packages/core/src/shared/constants.ts
+++ b/packages/core/src/shared/constants.ts
@@ -164,10 +164,6 @@ export const amazonQVscodeMarketplace =
*
* Moved here to resolve circular dependency issues.
*/
-export const crashMonitoringDirNames = {
- root: 'crashMonitoring',
- running: 'running',
- shutdown: 'shutdown',
-} as const
+export const crashMonitoringDirName = 'crashMonitoring'
export const amazonQTabSuffix = '(Generated by Amazon Q)'
diff --git a/packages/core/src/shared/crashMonitoring.ts b/packages/core/src/shared/crashMonitoring.ts
index 00fbb162862..b99bbef9436 100644
--- a/packages/core/src/shared/crashMonitoring.ts
+++ b/packages/core/src/shared/crashMonitoring.ts
@@ -15,9 +15,10 @@ import { isNewOsSession } from './utilities/osUtils'
import nodeFs from 'fs/promises'
import fs from './fs/fs'
import { getLogger } from './logger/logger'
-import { crashMonitoringDirNames } from './constants'
+import { crashMonitoringDirName } from './constants'
import { throwOnUnstableFileSystem } from './filesystemUtilities'
import { withRetries } from './utilities/functionUtils'
+import { TimeLag } from './utilities/timeoutUtils'
const className = 'CrashMonitoring'
@@ -112,15 +113,17 @@ export class CrashMonitoring {
try {
this.heartbeat = new Heartbeat(this.state, this.checkInterval, this.isDevMode)
+ this.heartbeat.onFailure(() => this.cleanup())
+
this.crashChecker = new CrashChecker(this.state, this.checkInterval, this.isDevMode, this.devLogger)
+ this.crashChecker.onFailure(() => this.cleanup())
await this.heartbeat.start()
await this.crashChecker.start()
} catch (error) {
emitFailure({ functionName: 'start', error })
try {
- this.crashChecker?.cleanup()
- await this.heartbeat?.cleanup()
+ await this.cleanup()
} catch {}
// Surface errors during development, otherwise it can be missed.
@@ -146,6 +149,11 @@ export class CrashMonitoring {
}
}
}
+
+ public async cleanup() {
+ this.crashChecker?.cleanup()
+ await this.heartbeat?.shutdown()
+ }
}
/**
@@ -154,15 +162,19 @@ export class CrashMonitoring {
*/
class Heartbeat {
private intervalRef: NodeJS.Timer | undefined
+ private _onFailure = new vscode.EventEmitter()
+ public onFailure: vscode.Event = this._onFailure.event
+ private readonly heartbeatInterval: number
+
constructor(
private readonly state: FileSystemState,
- private readonly checkInterval: number,
+ checkInterval: number,
private readonly isDevMode: boolean
- ) {}
+ ) {
+ this.heartbeatInterval = checkInterval / 2
+ }
public async start() {
- const heartbeatInterval = this.checkInterval / 2
-
// Send an initial heartbeat immediately
await withFailCtx('initialSendHeartbeat', () => this.state.sendHeartbeat())
@@ -172,38 +184,22 @@ class Heartbeat {
await this.state.sendHeartbeat()
} catch (e) {
try {
- await this.cleanup()
+ await this.shutdown()
emitFailure({ functionName: 'sendHeartbeatInterval', error: e })
} catch {}
if (this.isDevMode) {
throw e
}
+ this._onFailure.fire()
}
- }, heartbeatInterval)
+ }, this.heartbeatInterval)
}
/** Stops everything, signifying a graceful shutdown */
public async shutdown() {
globals.clock.clearInterval(this.intervalRef)
- return this.state.indicateGracefulShutdown()
- }
-
- /**
- * Safely attempts to clean up this heartbeat from the state to try and avoid
- * an incorrectly indicated crash. Use this on failures.
- *
- * ---
- *
- * IMPORTANT: This function must not throw as this function is run within a catch
- */
- public async cleanup() {
- try {
- await this.shutdown()
- } catch {}
- try {
- await this.state.clearHeartbeat()
- } catch {}
+ await this.state.indicateGracefulShutdown()
}
/** Mimics a crash, only for testing */
@@ -217,34 +213,55 @@ class Heartbeat {
*/
class CrashChecker {
private intervalRef: NodeJS.Timer | undefined
+ private _onFailure = new vscode.EventEmitter()
+ public onFailure = this._onFailure.event
constructor(
private readonly state: FileSystemState,
private readonly checkInterval: number,
private readonly isDevMode: boolean,
- private readonly devLogger: Logger | undefined
+ private readonly devLogger: Logger | undefined,
+ /**
+ * This class is required for the following edge case:
+ * 1. Heartbeat is sent
+ * 2. Computer goes to sleep for X minutes
+ * 3. Wake up computer. But before a new heartbeat can be sent, a crash checker (can be from another ext instance) runs
+ * and sees a stale heartbeat. It assumes a crash.
+ *
+ * Why? Intervals do not run while the computer is asleep, so the latest heartbeat has a "lag" since it wasn't able to send
+ * a new heartbeat.
+ * Then on wake, there is a racecondition for the next heartbeat to be sent before the next crash check. If the crash checker
+ * runs first it will incorrectly conclude a crash.
+ *
+ * Solution: Keep track of the lag, and then skip the next crash check if there was a lag. This will give time for the
+ * next heartbeat to be sent.
+ */
+ private readonly timeLag: TimeLag = new TimeLag()
) {}
public async start() {
{
this.devLogger?.debug(`crashMonitoring: checkInterval ${this.checkInterval}`)
+ this.timeLag.start()
+
// do an initial check
await withFailCtx('initialCrashCheck', () =>
- tryCheckCrash(this.state, this.checkInterval, this.isDevMode, this.devLogger)
+ tryCheckCrash(this.state, this.checkInterval, this.isDevMode, this.devLogger, this.timeLag)
)
// check on an interval
this.intervalRef = globals.clock.setInterval(async () => {
try {
- await tryCheckCrash(this.state, this.checkInterval, this.isDevMode, this.devLogger)
+ await tryCheckCrash(this.state, this.checkInterval, this.isDevMode, this.devLogger, this.timeLag)
} catch (e) {
emitFailure({ functionName: 'checkCrashInterval', error: e })
- this.cleanup()
if (this.isDevMode) {
throw e
}
+
+ this._onFailure.fire()
}
}, this.checkInterval)
}
@@ -255,46 +272,46 @@ class CrashChecker {
state: FileSystemState,
checkInterval: number,
isDevMode: boolean,
- devLogger: Logger | undefined
+ devLogger: Logger | undefined,
+ timeLag: TimeLag
) {
- // Iterate all known extensions and for each check if they have crashed
+ if (await timeLag.didLag()) {
+ timeLag.reset()
+ devLogger?.warn('crashMonitoring: SKIPPED check crash due to time lag')
+ return
+ }
+
+ // check each extension if it crashed
const knownExts = await state.getAllExts()
const runningExts: ExtInstanceHeartbeat[] = []
for (const ext of knownExts) {
+ // is still running
if (!isStoppedHeartbeats(ext, checkInterval)) {
runningExts.push(ext)
continue
}
- // Ext is not running anymore, handle appropriately depending on why it stopped running
- await state.handleExtNotRunning(ext, {
- onShutdown: async () => {
- // Nothing to do, just log info if necessary
- devLogger?.debug(
- `crashMonitoring: SHUTDOWN: following has gracefully shutdown: pid ${ext.extHostPid} + sessionId: ${ext.sessionId}`
- )
- },
- onCrash: async () => {
- // Debugger instances may incorrectly look like they crashed, so don't emit.
- // Example is if I hit the red square in the debug menu, it is a non-graceful shutdown. But the regular
- // 'x' button in the Debug IDE instance is a graceful shutdown.
- if (ext.isDebug) {
- devLogger?.debug(`crashMonitoring: DEBUG instance crashed: ${JSON.stringify(ext)}`)
- return
- }
-
- // This is the metric to let us know the extension crashed
- telemetry.session_end.emit({
- result: 'Failed',
- proxiedSessionId: ext.sessionId,
- reason: 'ExtHostCrashed',
- passive: true,
- })
-
- devLogger?.debug(
- `crashMonitoring: CRASH: following has crashed: pid ${ext.extHostPid} + sessionId: ${ext.sessionId}`
- )
- },
+ // did crash
+ await state.handleCrashedExt(ext, () => {
+ // Debugger instances may incorrectly look like they crashed, so don't emit.
+ // Example is if I hit the red square in the debug menu, it is a non-graceful shutdown. But the regular
+ // 'x' button in the Debug IDE instance is a graceful shutdown.
+ if (ext.isDebug) {
+ devLogger?.debug(`crashMonitoring: DEBUG instance crashed: ${JSON.stringify(ext)}`)
+ return
+ }
+
+ // This is the metric to let us know the extension crashed
+ telemetry.session_end.emit({
+ result: 'Failed',
+ proxiedSessionId: ext.sessionId,
+ reason: 'ExtHostCrashed',
+ passive: true,
+ })
+
+ devLogger?.debug(
+ `crashMonitoring: CRASH: following has crashed: pid ${ext.extHostPid} + sessionId: ${ext.sessionId}`
+ )
})
}
@@ -320,11 +337,12 @@ class CrashChecker {
/** Use this on failures to terminate the crash checker */
public cleanup() {
globals.clock.clearInterval(this.intervalRef)
+ this.timeLag.cleanup()
}
/** Mimics a crash, only for testing */
public testCrash() {
- globals.clock.clearInterval(this.intervalRef)
+ this.cleanup()
}
}
@@ -379,7 +397,7 @@ export class FileSystemState {
* IMORTANT: Use {@link crashMonitoringStateFactory} to make an instance
*/
constructor(protected readonly deps: MementoStateDependencies) {
- this.stateDirPath = path.join(this.deps.workDirPath, crashMonitoringDirNames.root)
+ this.stateDirPath = path.join(this.deps.workDirPath, crashMonitoringDirName)
this.deps.devLogger?.debug(`crashMonitoring: pid: ${this.deps.pid}`)
this.deps.devLogger?.debug(`crashMonitoring: sessionId: ${this.deps.sessionId.slice(0, 8)}-...`)
@@ -405,17 +423,16 @@ export class FileSystemState {
if (await this.deps.isStateStale()) {
await this.clearState()
}
+
+ await withFailCtx('init', () => fs.mkdir(this.stateDirPath))
}
// ------------------ Heartbeat methods ------------------
public async sendHeartbeat() {
- const extId = this.createExtId(this.ext)
-
try {
const func = async () => {
- const dir = await this.runningExtsDir()
await fs.writeFile(
- path.join(dir, extId),
+ this.makeStateFilePath(this.extId),
JSON.stringify({ ...this.ext, lastHeartbeat: this.deps.now() }, undefined, 4)
)
this.deps.devLogger?.debug(
@@ -437,7 +454,7 @@ export class FileSystemState {
}
/**
- * Signal that this extension is gracefully shutting down. This will prevent the IDE from thinking it crashed.
+ * Indicates that this extension instance has gracefully shutdown.
*
* IMPORTANT: This code is being run in `deactivate()` where VS Code api is not available. Due to this we cannot
* easily update the state to indicate a graceful shutdown. So the next best option is to write to a file on disk,
@@ -447,46 +464,26 @@ export class FileSystemState {
* function touches.
*/
public async indicateGracefulShutdown(): Promise {
- const dir = await this.shutdownExtsDir()
- await withFailCtx('writeShutdownFile', () => nodeFs.writeFile(path.join(dir, this.extId), ''))
+ // By removing the heartbeat entry, the crash checkers will not be able to find this entry anymore, making it
+ // impossible to report on since the file system is the source of truth
+ await withFailCtx('indicateGracefulShutdown', () =>
+ nodeFs.rm(this.makeStateFilePath(this.extId), { force: true })
+ )
}
// ------------------ Checker Methods ------------------
- /**
- * Signals the state that the given extension is not running, allowing the state to appropriately update
- * depending on a graceful shutdown or crash.
- *
- * NOTE: This does NOT run in the `deactivate()` method, so it CAN reliably use the VS Code FS api
- *
- * @param opts - functions to run depending on why the extension stopped running
- */
- public async handleExtNotRunning(
- ext: ExtInstance,
- opts: { onShutdown: () => Promise; onCrash: () => Promise }
- ): Promise {
- const extId = this.createExtId(ext)
- const shutdownFilePath = path.join(await this.shutdownExtsDir(), extId)
-
- if (await withFailCtx('existsShutdownFile', () => fs.exists(shutdownFilePath))) {
- await opts.onShutdown()
- // We intentionally do not clean up the file in shutdown since there may be another
- // extension may be doing the same thing in parallel, and would read the extension as
- // crashed since the file was missing. The file will be cleared on computer restart though.
-
- // TODO: Be smart and clean up the file after some time.
- } else {
- await opts.onCrash()
- }
-
- // Clean up the running extension file since it no longer exists
- await this.deleteHeartbeatFile(extId)
+ public async handleCrashedExt(ext: ExtInstance, fn: () => void) {
+ await withFailCtx('handleCrashedExt', async () => {
+ await this.deleteHeartbeatFile(ext)
+ fn()
+ })
}
- public async deleteHeartbeatFile(extId: ExtInstanceId) {
- const dir = await this.runningExtsDir()
+
+ private async deleteHeartbeatFile(ext: ExtInstanceId | ExtInstance) {
// Retry file deletion to prevent incorrect crash reports. Common Windows errors seen in telemetry: EPERM/EBUSY.
// See: https://github.com/aws/aws-toolkit-vscode/pull/5335
- await withRetries(() => withFailCtx('deleteStaleRunningFile', () => fs.delete(path.join(dir, extId))), {
+ await withRetries(() => withFailCtx('deleteStaleRunningFile', () => fs.delete(this.makeStateFilePath(ext))), {
maxRetries: 8,
delay: 100,
backoff: 2,
@@ -515,17 +512,9 @@ export class FileSystemState {
protected createExtId(ext: ExtInstance): ExtInstanceId {
return `${ext.extHostPid}_${ext.sessionId}`
}
- private async runningExtsDir(): Promise {
- const p = path.join(this.stateDirPath, crashMonitoringDirNames.running)
- // ensure the dir exists
- await withFailCtx('ensureRunningExtsDir', () => nodeFs.mkdir(p, { recursive: true }))
- return p
- }
- private async shutdownExtsDir() {
- const p = path.join(this.stateDirPath, crashMonitoringDirNames.shutdown)
- // Since this runs in `deactivate()` it cannot use the VS Code FS api
- await withFailCtx('ensureShutdownExtsDir', () => nodeFs.mkdir(p, { recursive: true }))
- return p
+ private makeStateFilePath(ext: ExtInstance | ExtInstanceId) {
+ const extId = typeof ext === 'string' ? ext : this.createExtId(ext)
+ return path.join(this.stateDirPath, extId)
}
public async clearState(): Promise {
this.deps.devLogger?.debug('crashMonitoring: CLEAR_STATE: Started')
@@ -538,7 +527,7 @@ export class FileSystemState {
const res = await withFailCtx('getAllExts', async () => {
// The file names are intentionally the IDs for easy mapping
const allExtIds: ExtInstanceId[] = await withFailCtx('readdir', async () =>
- (await fs.readdir(await this.runningExtsDir())).map((k) => k[0])
+ (await fs.readdir(this.stateDirPath)).map((k) => k[0])
)
const allExts = allExtIds.map>(async (extId: string) => {
@@ -549,7 +538,7 @@ export class FileSystemState {
() =>
withFailCtx('parseRunningExtFile', async () =>
ignoreBadFileError(async () => {
- const text = await fs.readFileText(path.join(await this.runningExtsDir(), extId))
+ const text = await fs.readFileText(this.makeStateFilePath(extId))
if (!text) {
return undefined
@@ -617,7 +606,10 @@ export type ExtInstance = {
isDebug?: boolean
}
-type ExtInstanceHeartbeat = ExtInstance & { lastHeartbeat: number }
+type ExtInstanceHeartbeat = ExtInstance & {
+ /** Timestamp of the last heartbeat in milliseconds */
+ lastHeartbeat: number
+}
function isExtHeartbeat(ext: unknown): ext is ExtInstanceHeartbeat {
return typeof ext === 'object' && ext !== null && 'lastHeartbeat' in ext && ext.lastHeartbeat !== undefined
diff --git a/packages/core/src/shared/errors.ts b/packages/core/src/shared/errors.ts
index 09597a8285f..95cebb03111 100644
--- a/packages/core/src/shared/errors.ts
+++ b/packages/core/src/shared/errors.ts
@@ -15,7 +15,7 @@ import type * as os from 'os'
import { CodeWhispererStreamingServiceException } from '@amzn/codewhisperer-streaming'
import { driveLetterRegex } from './utilities/pathUtils'
import { getLogger } from './logger/logger'
-import { crashMonitoringDirNames } from './constants'
+import { crashMonitoringDirName } from './constants'
let _username = 'unknown-user'
let _isAutomation = false
@@ -397,9 +397,7 @@ export function scrubNames(s: string, username?: string) {
'tmp',
'aws-toolkit-vscode',
'globalStorage', // from vscode globalStorageUri
- crashMonitoringDirNames.root,
- crashMonitoringDirNames.running,
- crashMonitoringDirNames.shutdown,
+ crashMonitoringDirName,
])
if (username && username.length > 2) {
diff --git a/packages/core/src/shared/utilities/timeoutUtils.ts b/packages/core/src/shared/utilities/timeoutUtils.ts
index f009b0676c9..18922067946 100644
--- a/packages/core/src/shared/utilities/timeoutUtils.ts
+++ b/packages/core/src/shared/utilities/timeoutUtils.ts
@@ -292,3 +292,70 @@ export function sleep(duration: number = 0): Promise {
const schedule = globals?.clock?.setTimeout ?? setTimeout
return new Promise((r) => schedule(r, Math.max(duration, 0)))
}
+
+/**
+ * Time lag occurs when the computer goes to sleep, and intervals cannot run on the expected
+ * cadence. This keeps track of that lag in cadence.
+ */
+export class TimeLag {
+ private intervalRef: NodeJS.Timer | undefined
+ private isCompleted: Promise | undefined
+ /** Resolves {@link isCompleted} when the next interval of lag checking is completed */
+ private setCompleted: (() => void) | undefined
+
+ /** The last timestamp we remember. If this is more than 1 {@link lagCheckInterval} we probably did a sleep+wake */
+ private latestTimestamp: number = 0
+ /** The accumulation of lag before the next crash checker interval, since the user can sleep+wake multiple times in between */
+ private totalLag: number = 0
+ private readonly lagCheckInterval = 1000
+
+ public start() {
+ this.reset() // initialize
+
+ // Every interval calculate the lag
+ this.intervalRef = globals.clock.setInterval(() => {
+ const expectedNow = this.latestTimestamp + this.lagCheckInterval
+ const actualNow = globals.clock.Date.now()
+ const lag = actualNow - expectedNow
+
+ // interval callback execution is not exact, so this buffer avoids micro lag from being
+ // considered actual lag
+ if (lag > 5000) {
+ this.totalLag += lag
+ }
+
+ this.latestTimestamp = Date.now()
+
+ // race condition between crash checker and lag checker on computer wake. So we have
+ // the crash checker block until this is completed.
+ this.setCompleted!()
+ this.setInProgress()
+ }, this.lagCheckInterval)
+ }
+
+ /** True if there is a time lag */
+ public async didLag() {
+ await this.isCompleted!
+ return this.totalLag > 0
+ }
+
+ /** Indicates the next time lag interval check is not completed */
+ private setInProgress() {
+ this.isCompleted = new Promise((resolve) => {
+ this.setCompleted = resolve
+ })
+ }
+
+ /**
+ * Call this once the user of this instance has handled the lag. If not done, {@link didLag} can
+ * be permanently true.
+ */
+ public reset() {
+ this.totalLag = 0
+ this.latestTimestamp = globals.clock.Date.now()
+ this.setInProgress()
+ }
+ public cleanup() {
+ globals.clock.clearInterval(this.intervalRef)
+ }
+}
diff --git a/packages/core/src/test/shared/crashMonitoring.test.ts b/packages/core/src/test/shared/crashMonitoring.test.ts
index 5802592ea8c..88c448a6cf4 100644
--- a/packages/core/src/test/shared/crashMonitoring.test.ts
+++ b/packages/core/src/test/shared/crashMonitoring.test.ts
@@ -30,7 +30,7 @@ export const crashMonitoringTest = async () => {
// Add some buffer since after 1 interval the work is actually done, including file i/o which may be slow.
// **IF FLAKY**, see if increasing the buffer helps.
- const oneInterval = checkInterval + 500
+ const oneInterval = checkInterval + 1000
/**
* Makes N "extension instances" that can be used for testing.
@@ -122,6 +122,7 @@ export const crashMonitoringTest = async () => {
await exts[i].ext.start()
}
+ // Crash all exts except the 0th one
for (let i = 1; i < extCount; i++) {
await exts[i].ext.crash()
latestCrashedExts.push(exts[i])
From 9d91760dc2b4a0ea611f5d7aaf9bccc5dbb595cd Mon Sep 17 00:00:00 2001
From: Maxim Hayes <149123719+hayemaxi@users.noreply.github.com>
Date: Thu, 17 Oct 2024 10:30:54 -0400
Subject: [PATCH 58/87] feat(notifications): initial rule engine (#5783)
Initial code for a rule engine to determine whether or not to show an
in-IDE notification. A notification is a JSON payload with a set amount
of criteria. The rule engine accepts context from the currently running
extension then determines if the notification payload's criteria will
fit the provided context.
The types match the commonly designed schema, but may change in future
commits.
Future work:
- More docs
- Updates to types and/or criteria
- Code that will use this
---
License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---
packages/core/src/notifications/rules.ts | 135 +++++
packages/core/src/notifications/types.ts | 88 ++++
packages/core/src/shared/telemetry/util.ts | 5 +-
.../core/src/test/notifications/rules.test.ts | 471 ++++++++++++++++++
4 files changed, 697 insertions(+), 2 deletions(-)
create mode 100644 packages/core/src/notifications/rules.ts
create mode 100644 packages/core/src/notifications/types.ts
create mode 100644 packages/core/src/test/notifications/rules.test.ts
diff --git a/packages/core/src/notifications/rules.ts b/packages/core/src/notifications/rules.ts
new file mode 100644
index 00000000000..6bd3d8f314c
--- /dev/null
+++ b/packages/core/src/notifications/rules.ts
@@ -0,0 +1,135 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as semver from 'semver'
+import globals from '../shared/extensionGlobals'
+import { ConditionalClause, RuleContext, DisplayIf, CriteriaCondition, ToolkitNotification } from './types'
+
+/**
+ * Evaluates if a given version fits into the parameters specified by a notification, e.g:
+ *
+ * extensionVersion: {
+ * type: 'range',
+ * lowerInclusive: '1.21.0'
+ * }
+ *
+ * will match all versions 1.21.0 and up.
+ *
+ * @param version the version to check
+ * @param condition the condition to check against
+ * @returns true if the version satisfies the condition
+ */
+function isValidVersion(version: string, condition: ConditionalClause): boolean {
+ switch (condition.type) {
+ case 'range': {
+ const lowerConstraint = !condition.lowerInclusive || semver.gte(version, condition.lowerInclusive)
+ const upperConstraint = !condition.upperExclusive || semver.lt(version, condition.upperExclusive)
+ return lowerConstraint && upperConstraint
+ }
+ case 'exactMatch':
+ return condition.values.some((v) => semver.eq(v, version))
+ case 'or':
+ /** Check case where any of the subconditions are true, i.e. one of multiple range or exactMatch conditions */
+ return condition.clauses.some((clause) => isValidVersion(version, clause))
+ default:
+ throw new Error(`Unknown clause type: ${(condition as any).type}`)
+ }
+}
+
+/**
+ * Determine whether or not to display a given notification based on whether the
+ * notification requirements fit the extension context provided on initialization.
+ *
+ * Usage:
+ * const myContext = {
+ * extensionVersion: '4.5.6',
+ * ...
+ * }
+ *
+ * const ruleEngine = new RuleEngine(myContext)
+ *
+ * notifications.forEach(n => {
+ * if (ruleEngine.shouldDisplayNotification(n)) {
+ * // process notification
+ * ...
+ * }
+ * })
+ *
+ */
+export class RuleEngine {
+ constructor(private readonly context: RuleContext) {}
+
+ public shouldDisplayNotification(payload: ToolkitNotification) {
+ return this.evaluate(payload.displayIf)
+ }
+
+ private evaluate(condition: DisplayIf): boolean {
+ if (condition.extensionId !== globals.context.extension.id) {
+ return false
+ }
+
+ if (condition.ideVersion) {
+ if (!isValidVersion(this.context.ideVersion, condition.ideVersion)) {
+ return false
+ }
+ }
+ if (condition.extensionVersion) {
+ if (!isValidVersion(this.context.extensionVersion, condition.extensionVersion)) {
+ return false
+ }
+ }
+
+ if (condition.additionalCriteria) {
+ for (const criteria of condition.additionalCriteria) {
+ if (!this.evaluateRule(criteria)) {
+ return false
+ }
+ }
+ }
+
+ return true
+ }
+
+ private evaluateRule(criteria: CriteriaCondition) {
+ const expected = criteria.values
+ const expectedSet = new Set(expected)
+
+ const isExpected = (i: string) => expectedSet.has(i)
+ const hasAnyOfExpected = (i: string[]) => i.some((v) => expectedSet.has(v))
+ const isSuperSetOfExpected = (i: string[]) => {
+ const s = new Set(i)
+ return expected.every((v) => s.has(v))
+ }
+ const isEqualSetToExpected = (i: string[]) => {
+ const s = new Set(i)
+ return expected.every((v) => s.has(v)) && i.every((v) => expectedSet.has(v))
+ }
+
+ // Maybe we could abstract these out into some strategy pattern with classes.
+ // But this list is short and its unclear if we need to expand it further.
+ // Also, we might replace this with a common implementation amongst the toolkits.
+ // So... YAGNI
+ switch (criteria.type) {
+ case 'OS':
+ return isExpected(this.context.os)
+ case 'ComputeEnv':
+ return isExpected(this.context.computeEnv)
+ case 'AuthType':
+ return hasAnyOfExpected(this.context.authTypes)
+ case 'AuthRegion':
+ return hasAnyOfExpected(this.context.authRegions)
+ case 'AuthState':
+ return hasAnyOfExpected(this.context.authStates)
+ case 'AuthScopes':
+ return isEqualSetToExpected(this.context.authScopes)
+ case 'InstalledExtensions':
+ return isSuperSetOfExpected(this.context.installedExtensions)
+ case 'ActiveExtensions':
+ return isSuperSetOfExpected(this.context.activeExtensions)
+ default:
+ throw new Error(`Unknown criteria type: ${criteria.type}`)
+ }
+ }
+}
diff --git a/packages/core/src/notifications/types.ts b/packages/core/src/notifications/types.ts
new file mode 100644
index 00000000000..bab3170dd87
--- /dev/null
+++ b/packages/core/src/notifications/types.ts
@@ -0,0 +1,88 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as vscode from 'vscode'
+import { EnvType, OperatingSystem } from '../shared/telemetry/util'
+
+/** Types of information that we can use to determine whether to show a notification or not. */
+export type Criteria =
+ | 'OS'
+ | 'ComputeEnv'
+ | 'AuthType'
+ | 'AuthRegion'
+ | 'AuthState'
+ | 'AuthScopes'
+ | 'InstalledExtensions'
+ | 'ActiveExtensions'
+
+/** Generic condition where the type determines how the values are evaluated. */
+export interface CriteriaCondition {
+ readonly type: Criteria
+ readonly values: string[]
+}
+
+/** One of the subconditions (clauses) must match to be valid. */
+export interface OR {
+ readonly type: 'or'
+ readonly clauses: (Range | ExactMatch)[]
+}
+
+/** Version must be within the bounds to be valid. Missing bound indicates that bound is open-ended. */
+export interface Range {
+ readonly type: 'range'
+ readonly lowerInclusive?: string // null means "-inf"
+ readonly upperExclusive?: string // null means "+inf"
+}
+
+/** Version must be equal. */
+export interface ExactMatch {
+ readonly type: 'exactMatch'
+ readonly values: string[]
+}
+
+export type ConditionalClause = Range | ExactMatch | OR
+
+/** How to display the notification. */
+export interface UIRenderInstructions {
+ content: {
+ [`en-US`]: {
+ title: string
+ description: string
+ }
+ }
+ // TODO actions
+}
+
+/** Condition/criteria section of a notification. */
+export interface DisplayIf {
+ extensionId: string
+ ideVersion?: ConditionalClause
+ extensionVersion?: ConditionalClause
+ additionalCriteria?: CriteriaCondition[]
+}
+
+export interface ToolkitNotification {
+ id: string
+ displayIf: DisplayIf
+ uiRenderInstructions: UIRenderInstructions
+}
+
+export interface Notifications {
+ schemaVersion: string
+ notifications: ToolkitNotification[]
+}
+
+export interface RuleContext {
+ readonly ideVersion: typeof vscode.version
+ readonly extensionVersion: string
+ readonly os: OperatingSystem
+ readonly computeEnv: EnvType
+ readonly authTypes: string[]
+ readonly authRegions: string[]
+ readonly authStates: string[]
+ readonly authScopes: string[]
+ readonly installedExtensions: string[]
+ readonly activeExtensions: string[]
+}
diff --git a/packages/core/src/shared/telemetry/util.ts b/packages/core/src/shared/telemetry/util.ts
index 08a4bed0807..bd6b5b206a6 100644
--- a/packages/core/src/shared/telemetry/util.ts
+++ b/packages/core/src/shared/telemetry/util.ts
@@ -226,7 +226,7 @@ export function getUserAgent(
* NOTES:
* - append `-amzn` for any environment internal to Amazon
*/
-type EnvType =
+export type EnvType =
| 'cloud9'
| 'cloud9-codecatalyst'
| 'cloudDesktop-amzn'
@@ -322,12 +322,13 @@ export function getOptOutPreference() {
return globals.telemetry.telemetryEnabled ? 'OPTIN' : 'OPTOUT'
}
+export type OperatingSystem = 'MAC' | 'WINDOWS' | 'LINUX'
/**
* Useful for populating the sendTelemetryEvent request from codewhisperer's api for publishing custom telemetry events for AB Testing.
*
* Returns one of the enum values of the OperatingSystem model (see SendTelemetryRequest model in the codebase)
*/
-export function getOperatingSystem(): 'MAC' | 'WINDOWS' | 'LINUX' {
+export function getOperatingSystem(): OperatingSystem {
const osId = os.platform() // 'darwin', 'win32', 'linux', etc.
if (osId === 'darwin') {
return 'MAC'
diff --git a/packages/core/src/test/notifications/rules.test.ts b/packages/core/src/test/notifications/rules.test.ts
new file mode 100644
index 00000000000..f1772817024
--- /dev/null
+++ b/packages/core/src/test/notifications/rules.test.ts
@@ -0,0 +1,471 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert'
+import { RuleEngine } from '../../notifications/rules'
+import { DisplayIf, ToolkitNotification, RuleContext } from '../../notifications/types'
+import { globals } from '../../shared'
+
+// TODO: remove auth page and tests
+describe('Notifications Rule Engine', function () {
+ const context: RuleContext = {
+ ideVersion: '1.83.0',
+ extensionVersion: '1.20.0',
+ os: 'LINUX',
+ computeEnv: 'local',
+ authTypes: ['builderId'],
+ authRegions: ['us-east-1'],
+ authStates: ['connected'],
+ authScopes: ['codewhisperer:completions', 'codewhisperer:analysis'],
+ installedExtensions: ['ext1', 'ext2', 'ext3'],
+ activeExtensions: ['ext1', 'ext2'],
+ }
+
+ const ruleEngine = new RuleEngine(context)
+
+ function buildNotification(criteria: Omit): ToolkitNotification {
+ return {
+ id: 'bd22f116-edd4-4e80-8f1f-ec7340159016',
+ displayIf: { extensionId: globals.context.extension.id, ...criteria },
+ uiRenderInstructions: {
+ content: {
+ [`en-US`]: {
+ title: 'Something crazy is happening!',
+ description: 'Something crazy is happening! Please update your extension.',
+ },
+ },
+ },
+ }
+ }
+
+ it('should display notification with no criteria', function () {
+ const notification = buildNotification({})
+ assert.equal(ruleEngine.shouldDisplayNotification(notification), true)
+ })
+
+ it('should display notification with version exact criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ ideVersion: {
+ type: 'exactMatch',
+ values: ['1.82.0', '1.83.0'],
+ },
+ })
+ ),
+ true
+ )
+
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ extensionVersion: {
+ type: 'exactMatch',
+ values: ['1.19.0', '1.20.0'],
+ },
+ })
+ ),
+ true
+ )
+ })
+
+ it('should NOT display notification with invalid version exact criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ ideVersion: {
+ type: 'exactMatch',
+ values: ['1.82.0', '1.84.0'],
+ },
+ })
+ ),
+ false
+ )
+
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ extensionVersion: {
+ type: 'exactMatch',
+ values: ['1.19.0', '1.21.0'],
+ },
+ })
+ ),
+ false
+ )
+ })
+
+ it('should display notification with version range criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ extensionVersion: {
+ type: 'range',
+ lowerInclusive: '1.20.0',
+ upperExclusive: '1.21.0',
+ },
+ })
+ ),
+ true
+ )
+
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ extensionVersion: {
+ type: 'range',
+ upperExclusive: '1.23.0',
+ },
+ })
+ ),
+ true
+ )
+
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ extensionVersion: {
+ type: 'range',
+ lowerInclusive: '1.0.0',
+ },
+ })
+ ),
+ true
+ )
+ })
+
+ it('should NOT display notification with invalid version range criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ extensionVersion: {
+ type: 'range',
+ lowerInclusive: '1.18.0',
+ upperExclusive: '1.20.0',
+ },
+ })
+ ),
+ false
+ )
+ })
+
+ it('should display notification with version OR criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ extensionVersion: {
+ type: 'or',
+ clauses: [
+ {
+ type: 'exactMatch',
+ values: ['1.18.0', '1.19.0'],
+ },
+ {
+ type: 'range',
+ lowerInclusive: '1.18.0',
+ upperExclusive: '1.21.0',
+ },
+ ],
+ },
+ })
+ ),
+ true
+ )
+ })
+
+ it('should NOT display notification with invalid version OR criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ extensionVersion: {
+ type: 'or',
+ clauses: [
+ {
+ type: 'exactMatch',
+ values: ['1.18.0', '1.19.0'],
+ },
+ {
+ type: 'range',
+ lowerInclusive: '1.18.0',
+ upperExclusive: '1.20.0',
+ },
+ ],
+ },
+ })
+ ),
+ false
+ )
+ })
+
+ it('should display notification for OS criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ additionalCriteria: [{ type: 'OS', values: ['LINUX', 'MAC'] }],
+ })
+ ),
+ true
+ )
+ })
+
+ it('should NOT display notification for invalid OS criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ additionalCriteria: [{ type: 'OS', values: ['MAC'] }],
+ })
+ ),
+ false
+ )
+ })
+
+ it('should display notification for ComputeEnv criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ additionalCriteria: [{ type: 'ComputeEnv', values: ['local', 'ec2'] }],
+ })
+ ),
+ true
+ )
+ })
+
+ it('should NOT display notification for invalid ComputeEnv criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ additionalCriteria: [{ type: 'ComputeEnv', values: ['ec2'] }],
+ })
+ ),
+ false
+ )
+ })
+
+ it('should display notification for AuthType criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ additionalCriteria: [{ type: 'AuthType', values: ['builderId', 'iamIdentityCenter'] }],
+ })
+ ),
+ true
+ )
+ })
+
+ it('should NOT display notification for invalid AuthType criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ additionalCriteria: [{ type: 'AuthType', values: ['iamIdentityCenter'] }],
+ })
+ ),
+ false
+ )
+ })
+
+ it('should display notification for AuthRegion criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ additionalCriteria: [{ type: 'AuthRegion', values: ['us-east-1', 'us-west-2'] }],
+ })
+ ),
+ true
+ )
+ })
+
+ it('should NOT display notification for invalid AuthRegion criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ additionalCriteria: [{ type: 'AuthRegion', values: ['us-west-2'] }],
+ })
+ ),
+ false
+ )
+ })
+
+ it('should display notification for AuthState criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ additionalCriteria: [{ type: 'AuthState', values: ['connected'] }],
+ })
+ ),
+ true
+ )
+ })
+
+ it('should NOT display notification for invalid AuthState criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ additionalCriteria: [{ type: 'AuthState', values: ['notConnected'] }],
+ })
+ ),
+ false
+ )
+ })
+
+ it('should display notification for AuthScopes criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ additionalCriteria: [
+ { type: 'AuthScopes', values: ['codewhisperer:completions', 'codewhisperer:analysis'] },
+ ],
+ })
+ ),
+ true
+ )
+ })
+
+ it('should NOT display notification for invalid AuthScopes criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ additionalCriteria: [{ type: 'AuthScopes', values: ['codewhisperer:completions'] }],
+ })
+ ),
+ false
+ )
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ additionalCriteria: [
+ {
+ type: 'AuthScopes',
+ values: ['codewhisperer:completions', 'codewhisperer:analysis', 'sso:account:access'],
+ },
+ ],
+ })
+ ),
+ false
+ )
+ })
+
+ it('should display notification for InstalledExtensions criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ additionalCriteria: [{ type: 'InstalledExtensions', values: ['ext1', 'ext2'] }],
+ })
+ ),
+ true
+ )
+ })
+
+ it('should NOT display notification for invalid InstalledExtensions criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ additionalCriteria: [{ type: 'InstalledExtensions', values: ['ext1', 'ext2', 'unkownExtension'] }],
+ })
+ ),
+ false
+ )
+ })
+
+ it('should display notification for ActiveExtensions criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ additionalCriteria: [{ type: 'ActiveExtensions', values: ['ext1', 'ext2'] }],
+ })
+ ),
+ true
+ )
+ })
+
+ it('should NOT display notification for invalid ActiveExtensions criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ additionalCriteria: [{ type: 'ActiveExtensions', values: ['ext1', 'ext2', 'unknownExtension'] }],
+ })
+ ),
+ false
+ )
+ })
+
+ it('should display notification for combined criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ ideVersion: {
+ type: 'or',
+ clauses: [
+ {
+ type: 'range',
+ lowerInclusive: '1.70.0',
+ upperExclusive: '1.81.0',
+ },
+ {
+ type: 'range',
+ lowerInclusive: '1.81.0',
+ upperExclusive: '1.83.3',
+ },
+ ],
+ },
+ extensionVersion: {
+ type: 'exactMatch',
+ values: ['1.19.0', '1.20.0'],
+ },
+ additionalCriteria: [
+ { type: 'OS', values: ['LINUX', 'MAC'] },
+ { type: 'ComputeEnv', values: ['local', 'ec2'] },
+ { type: 'AuthType', values: ['builderId', 'iamIdentityCenter'] },
+ { type: 'AuthRegion', values: ['us-east-1', 'us-west-2'] },
+ { type: 'AuthState', values: ['connected'] },
+ { type: 'AuthScopes', values: ['codewhisperer:completions', 'codewhisperer:analysis'] },
+ { type: 'InstalledExtensions', values: ['ext1', 'ext2'] },
+ { type: 'ActiveExtensions', values: ['ext1', 'ext2'] },
+ ],
+ })
+ ),
+ true
+ )
+ })
+
+ it('should NOT display notification for invalid combined criteria', function () {
+ assert.equal(
+ ruleEngine.shouldDisplayNotification(
+ buildNotification({
+ ideVersion: {
+ type: 'or',
+ clauses: [
+ {
+ type: 'range',
+ lowerInclusive: '1.70.0',
+ upperExclusive: '1.81.0',
+ },
+ {
+ type: 'range',
+ lowerInclusive: '1.80.0',
+ upperExclusive: '1.83.3',
+ },
+ ],
+ },
+ extensionVersion: {
+ type: 'exactMatch',
+ values: ['1.19.0', '1.20.0'],
+ },
+ additionalCriteria: [
+ { type: 'OS', values: ['LINUX', 'MAC'] },
+ { type: 'AuthType', values: ['builderId', 'iamIdentityCenter'] },
+ { type: 'AuthRegion', values: ['us-east-1', 'us-west-2'] },
+ { type: 'AuthState', values: ['connected'] },
+ { type: 'AuthScopes', values: ['codewhisperer:completions', 'codewhisperer:analysis'] },
+ { type: 'InstalledExtensions', values: ['ex1', 'ext2'] },
+ { type: 'ActiveExtensions', values: ['ext1', 'ext2'] },
+
+ { type: 'ComputeEnv', values: ['ec2'] }, // no 'local'
+ ],
+ })
+ ),
+ false
+ )
+ })
+})
From a12bef27666c07d15d9bc6bfa8e0f9b650de296c Mon Sep 17 00:00:00 2001
From: Nikolas Komonen <118216176+nkomonen-amazon@users.noreply.github.com>
Date: Thu, 17 Oct 2024 13:06:37 -0400
Subject: [PATCH 59/87] test(crash): Crash minor fixes (#5804)
See each commit message for revelant fix
---
License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---------
Signed-off-by: nkomonen-amazon
---
packages/core/src/shared/crashMonitoring.ts | 45 ++++-
.../src/test/shared/crashMonitoring.test.ts | 185 +++++++++++++-----
2 files changed, 167 insertions(+), 63 deletions(-)
diff --git a/packages/core/src/shared/crashMonitoring.ts b/packages/core/src/shared/crashMonitoring.ts
index b99bbef9436..3336a1e72fc 100644
--- a/packages/core/src/shared/crashMonitoring.ts
+++ b/packages/core/src/shared/crashMonitoring.ts
@@ -69,7 +69,8 @@ export class CrashMonitoring {
private readonly checkInterval: number,
private readonly isDevMode: boolean,
private readonly isAutomation: boolean,
- private readonly devLogger: Logger | undefined
+ private readonly devLogger: Logger | undefined,
+ private readonly timeLag: TimeLag
) {}
static #didTryCreate = false
@@ -92,7 +93,8 @@ export class CrashMonitoring {
DevSettings.instance.get('crashCheckInterval', 1000 * 60 * 10), // check every 10 minutes
isDevMode,
isAutomation(),
- devModeLogger
+ devModeLogger,
+ new TimeLag()
))
} catch (error) {
emitFailure({ functionName: 'instance', error })
@@ -115,7 +117,13 @@ export class CrashMonitoring {
this.heartbeat = new Heartbeat(this.state, this.checkInterval, this.isDevMode)
this.heartbeat.onFailure(() => this.cleanup())
- this.crashChecker = new CrashChecker(this.state, this.checkInterval, this.isDevMode, this.devLogger)
+ this.crashChecker = new CrashChecker(
+ this.state,
+ this.checkInterval,
+ this.isDevMode,
+ this.devLogger,
+ this.timeLag
+ )
this.crashChecker.onFailure(() => this.cleanup())
await this.heartbeat.start()
@@ -236,7 +244,7 @@ class CrashChecker {
* Solution: Keep track of the lag, and then skip the next crash check if there was a lag. This will give time for the
* next heartbeat to be sent.
*/
- private readonly timeLag: TimeLag = new TimeLag()
+ private readonly timeLag: TimeLag
) {}
public async start() {
@@ -391,7 +399,7 @@ export async function crashMonitoringStateFactory(deps = getDefaultDependencies(
* - is not truly reliable since filesystems are not reliable
*/
export class FileSystemState {
- private readonly stateDirPath: string
+ public readonly stateDirPath: string
/**
* IMORTANT: Use {@link crashMonitoringStateFactory} to make an instance
@@ -512,9 +520,10 @@ export class FileSystemState {
protected createExtId(ext: ExtInstance): ExtInstanceId {
return `${ext.extHostPid}_${ext.sessionId}`
}
+ private readonly fileSuffix = 'running'
private makeStateFilePath(ext: ExtInstance | ExtInstanceId) {
const extId = typeof ext === 'string' ? ext : this.createExtId(ext)
- return path.join(this.stateDirPath, extId)
+ return path.join(this.stateDirPath, extId + `.${this.fileSuffix}`)
}
public async clearState(): Promise {
this.deps.devLogger?.debug('crashMonitoring: CLEAR_STATE: Started')
@@ -525,10 +534,26 @@ export class FileSystemState {
}
public async getAllExts(): Promise {
const res = await withFailCtx('getAllExts', async () => {
- // The file names are intentionally the IDs for easy mapping
- const allExtIds: ExtInstanceId[] = await withFailCtx('readdir', async () =>
- (await fs.readdir(this.stateDirPath)).map((k) => k[0])
- )
+ // Read all the exts from the filesystem, deserializing as needed
+ const allExtIds: ExtInstanceId[] = await withFailCtx('readdir', async () => {
+ const filesInDir = await fs.readdir(this.stateDirPath)
+ const relevantFiles = filesInDir.filter((file: [string, vscode.FileType]) => {
+ const name = file[0]
+ const type = file[1]
+ if (type !== vscode.FileType.File) {
+ return false
+ }
+ if (path.extname(name) !== `.${this.fileSuffix}`) {
+ return false
+ }
+ return true
+ })
+ const idsFromFileNames = relevantFiles.map((file: [string, vscode.FileType]) => {
+ const name = file[0]
+ return name.split('.')[0]
+ })
+ return idsFromFileNames
+ })
const allExts = allExtIds.map>(async (extId: string) => {
// Due to a race condition, a separate extension instance may have removed this file by this point. It is okay since
diff --git a/packages/core/src/test/shared/crashMonitoring.test.ts b/packages/core/src/test/shared/crashMonitoring.test.ts
index 88c448a6cf4..e231a37a400 100644
--- a/packages/core/src/test/shared/crashMonitoring.test.ts
+++ b/packages/core/src/test/shared/crashMonitoring.test.ts
@@ -9,6 +9,10 @@ import globals from '../../shared/extensionGlobals'
import { CrashMonitoring, ExtInstance, crashMonitoringStateFactory } from '../../shared/crashMonitoring'
import { isCI } from '../../shared/vscode/env'
import { getLogger } from '../../shared/logger/logger'
+import { TimeLag } from '../../shared/utilities/timeoutUtils'
+import { SinonSandbox, createSandbox } from 'sinon'
+import { fs } from '../../shared'
+import path from 'path'
class TestCrashMonitoring extends CrashMonitoring {
public constructor(...deps: ConstructorParameters) {
@@ -24,6 +28,8 @@ class TestCrashMonitoring extends CrashMonitoring {
export const crashMonitoringTest = async () => {
let testFolder: TestFolder
let spawnedExtensions: TestCrashMonitoring[]
+ let timeLag: TimeLag
+ let sandbox: SinonSandbox
// Scale down the default interval we heartbeat and check for crashes to something much short for testing.
const checkInterval = 200
@@ -38,48 +44,54 @@ export const crashMonitoringTest = async () => {
* 1:1 mapping between Crash Reporting instances and the Extension instances.
*/
async function makeTestExtensions(amount: number) {
- const devLogger = getLogger()
-
const extensions: TestExtension[] = []
for (let i = 0; i < amount; i++) {
- const sessionId = `sessionId-${i}`
- const pid = Number(String(i).repeat(6))
- const state = await crashMonitoringStateFactory({
- workDirPath: testFolder.path,
- isStateStale: async () => false,
- pid,
- sessionId: sessionId,
- now: () => globals.clock.Date.now(),
- memento: globals.globalState,
- isDevMode: true,
- devLogger,
- })
- const ext = new TestCrashMonitoring(state, checkInterval, true, false, devLogger)
- spawnedExtensions.push(ext)
- const metadata = {
- extHostPid: pid,
- sessionId,
- lastHeartbeat: globals.clock.Date.now(),
- isDebug: undefined,
- }
- extensions[i] = { ext, metadata }
+ extensions[i] = await makeTestExtension(i, new TimeLag())
}
return extensions
}
+ async function makeTestExtension(id: number, timeLag: TimeLag, opts?: { isStateStale: () => Promise }) {
+ const isStateStale = opts?.isStateStale ?? (() => Promise.resolve(false))
+ const sessionId = `sessionId-${id}`
+ const pid = Number(String(id).repeat(6))
+
+ const state = await crashMonitoringStateFactory({
+ workDirPath: testFolder.path,
+ isStateStale,
+ pid,
+ sessionId: sessionId,
+ now: () => globals.clock.Date.now(),
+ memento: globals.globalState,
+ isDevMode: true,
+ devLogger: getLogger(),
+ })
+ const ext = new TestCrashMonitoring(state, checkInterval, true, false, getLogger(), timeLag)
+ spawnedExtensions.push(ext)
+ const metadata = {
+ extHostPid: pid,
+ sessionId,
+ lastHeartbeat: globals.clock.Date.now(),
+ isDebug: undefined,
+ }
+ return { ext, metadata }
+ }
+
beforeEach(async function () {
testFolder = await TestFolder.create()
spawnedExtensions = []
+ timeLag = new TimeLag()
+ sandbox = createSandbox()
})
afterEach(async function () {
// clean up all running instances
spawnedExtensions?.forEach((e) => e.crash())
+ timeLag.cleanup()
+ sandbox.restore()
})
it('graceful shutdown no metric emitted', async function () {
- // this.retries(3)
-
const exts = await makeTestExtensions(2)
await exts[0].ext.start()
@@ -95,9 +107,7 @@ export const crashMonitoringTest = async () => {
assertTelemetry('session_end', [])
})
- it('single running instances crashes, so nothing is reported, but a new instaces appears and reports', async function () {
- // this.retries(3)
-
+ it('single running instance crashes, so nothing is reported, but a new instaces appears and reports', async function () {
const exts = await makeTestExtensions(2)
await exts[0].ext.start()
@@ -112,34 +122,11 @@ export const crashMonitoringTest = async () => {
assertCrashedExtensions([exts[0]])
})
- it('start the first extension, then start many subsequent ones and crash them all at once', async function () {
- // this.retries(3)
- const latestCrashedExts: TestExtension[] = []
-
- const extCount = 10
- const exts = await makeTestExtensions(extCount)
- for (let i = 0; i < extCount; i++) {
- await exts[i].ext.start()
- }
-
- // Crash all exts except the 0th one
- for (let i = 1; i < extCount; i++) {
- await exts[i].ext.crash()
- latestCrashedExts.push(exts[i])
- }
-
- // Give some extra time since there is a lot of file i/o
- await awaitIntervals(oneInterval * 2)
-
- assertCrashedExtensions(latestCrashedExts)
- })
-
- it('the Primary checker crashes and another checker is promoted to Primary', async function () {
- // this.retries(3)
+ it('multiple running instances start+crash at different times, but another instance always reports', async function () {
const latestCrashedExts: TestExtension[] = []
const exts = await makeTestExtensions(4)
- // Ext 0 is the Primary checker
+
await exts[0].ext.start()
await awaitIntervals(oneInterval)
@@ -166,6 +153,73 @@ export const crashMonitoringTest = async () => {
assertCrashedExtensions(latestCrashedExts)
})
+ it('clears the state when a new os session is determined', async function () {
+ const exts = await makeTestExtensions(1)
+
+ // Start an extension then crash it
+ await exts[0].ext.start()
+ await exts[0].ext.crash()
+ await awaitIntervals(oneInterval)
+ // There is no other active instance to report the issue
+ assertTelemetry('session_end', [])
+
+ // This extension clears the state due to it being stale, not reporting the previously crashed ext
+ const ext1 = await makeTestExtension(1, timeLag, { isStateStale: () => Promise.resolve(true) })
+ await ext1.ext.start()
+ await awaitIntervals(oneInterval * 1)
+ assertCrashedExtensions([])
+ })
+
+ it('start the first extension, then start many subsequent ones and crash them all at once', async function () {
+ const latestCrashedExts: TestExtension[] = []
+
+ const extCount = 10
+ const exts = await makeTestExtensions(extCount)
+ for (let i = 0; i < extCount; i++) {
+ await exts[i].ext.start()
+ }
+
+ // Crash all exts except the 0th one
+ for (let i = 1; i < extCount; i++) {
+ await exts[i].ext.crash()
+ latestCrashedExts.push(exts[i])
+ }
+
+ // Give some extra time since there is a lot of file i/o
+ await awaitIntervals(oneInterval * 2)
+
+ assertCrashedExtensions(latestCrashedExts)
+ })
+
+ it('does not check for crashes when there is a time lag', async function () {
+ // This test handles the case for a users computer doing a sleep+wake and
+ // then a crash was incorrectly reported since a new heartbeat could not be sent in time
+
+ const timeLagStub = sandbox.stub(timeLag)
+ timeLagStub.start.resolves()
+ timeLagStub.didLag.resolves(false)
+
+ // Load up a crash
+ const ext0 = await makeTestExtension(0, timeLagStub as unknown as TimeLag)
+ await ext0.ext.start()
+ await ext0.ext.crash()
+
+ const ext1 = await makeTestExtension(1, timeLagStub as unknown as TimeLag)
+ await ext1.ext.start()
+
+ // Indicate that we have a time lag, and until it returns false
+ // we will skip crash checking
+ timeLagStub.didLag.resolves(true)
+ await awaitIntervals(oneInterval)
+ assertCrashedExtensions([])
+ await awaitIntervals(oneInterval)
+ assertCrashedExtensions([])
+ // Now that the time lag is true, we will check for a crash
+ timeLagStub.didLag.resolves(false)
+ await awaitIntervals(oneInterval)
+ assertCrashedExtensions([ext0])
+ })
+
/**
* Something like the following code can switch contexts early and the test will
* finish before it has completed. Certain async functions that may take longer to run
@@ -219,6 +273,31 @@ export const crashMonitoringTest = async () => {
function deduplicate(array: T[], predicate: (a: T, b: T) => boolean): T[] {
return array.filter((item, index, self) => index === self.findIndex((t) => predicate(item, t)))
}
+
+ describe('FileSystemState', async function () {
+ it('ignores irrelevant files in state', async function () {
+ const state = await crashMonitoringStateFactory({
+ workDirPath: testFolder.path,
+ isStateStale: () => Promise.resolve(false),
+ pid: 1111,
+ sessionId: 'sessionId_1111',
+ now: () => globals.clock.Date.now(),
+ memento: globals.globalState,
+ isDevMode: true,
+ devLogger: getLogger(),
+ })
+ const stateDirPath = state.stateDirPath
+
+ assert.deepStrictEqual((await fs.readdir(stateDirPath)).length, 0)
+ await fs.writeFile(path.join(stateDirPath, 'ignoreMe.json'), '')
+ await fs.mkdir(path.join(stateDirPath, 'ignoreMe'))
+ await state.sendHeartbeat() // creates a relevant file in the state
+ assert.deepStrictEqual((await fs.readdir(stateDirPath)).length, 3)
+
+ const result = await state.getAllExts()
+ assert.deepStrictEqual(result.length, 1)
+ })
+ })
}
// This test is slow, so we only want to run it locally and not in CI. It will be run in the integ CI tests though.
;(isCI() ? describe.skip : describe)('CrashReporting', crashMonitoringTest)
From 3c295184f2adcecda9cc906fafa1efaedbbe67de Mon Sep 17 00:00:00 2001
From: Vikash Agrawal
Date: Thu, 17 Oct 2024 10:33:01 -0700
Subject: [PATCH 60/87] fix(amazonq): display animation for Apply Diff and View
Diff buttons (#5802)
Add animation for View Diff and Apply Diff buttons.
Demo -
https://github.com/user-attachments/assets/05092447-be86-4dc8-89b5-ecb7c4bfee0f
License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---
packages/core/resources/css/amazonq-chat.css | 25 ++++++++
.../webview/generators/webViewContent.ts | 57 +++++++++++++------
2 files changed, 65 insertions(+), 17 deletions(-)
create mode 100644 packages/core/resources/css/amazonq-chat.css
diff --git a/packages/core/resources/css/amazonq-chat.css b/packages/core/resources/css/amazonq-chat.css
new file mode 100644
index 00000000000..5e954bba89f
--- /dev/null
+++ b/packages/core/resources/css/amazonq-chat.css
@@ -0,0 +1,25 @@
+/* Pulsating effect */
+@keyframes pulsate {
+ 0% {
+ opacity: 0;
+ }
+ 50% {
+ opacity: 0.15;
+ }
+ 100% {
+ opacity: 0;
+ }
+}
+
+body[data-feature-viewdiffinchat='TREATMENT'] {
+ .mynah-syntax-highlighter-copy-buttons button:nth-of-type(2)::after, /* Accept Diff and Insert At Cursor */
+ .mynah-syntax-highlighter-copy-buttons button:nth-of-type(3)::after {
+ /* View Diff */
+ content: '';
+ animation-name: pulsate;
+ animation-duration: 1.5s;
+ animation-timing-function: ease-in-out;
+ animation-iteration-count: 2;
+ transform: translate3d(0%, 0, 0);
+ }
+}
diff --git a/packages/core/src/amazonq/webview/generators/webViewContent.ts b/packages/core/src/amazonq/webview/generators/webViewContent.ts
index 494938b5911..0d67fffbec4 100644
--- a/packages/core/src/amazonq/webview/generators/webViewContent.ts
+++ b/packages/core/src/amazonq/webview/generators/webViewContent.ts
@@ -9,6 +9,20 @@ import { AuthUtil } from '../../../codewhisperer/util/authUtil'
import { FeatureConfigProvider, FeatureContext, globals } from '../../../shared'
export class WebViewContentGenerator {
+ private async generateFeatureConfigsData(): Promise {
+ let featureConfigs = new Map()
+ try {
+ await FeatureConfigProvider.instance.fetchFeatureConfigs()
+ featureConfigs = FeatureConfigProvider.getFeatureConfigs()
+ } catch (error) {
+ // eslint-disable-next-line aws-toolkits/no-console-log
+ console.error('Error fetching feature configs:', error)
+ }
+
+ // Convert featureConfigs to a string suitable for data-features
+ return JSON.stringify(Array.from(featureConfigs.entries()))
+ }
+
public async generate(extensionURI: Uri, webView: Webview): Promise {
const entrypoint = process.env.WEBPACK_DEVELOPER_SERVER
? 'http: localhost'
@@ -17,14 +31,25 @@ export class WebViewContentGenerator {
const contentPolicy = `default-src ${entrypoint} data: blob: 'unsafe-inline';
script-src ${entrypoint} filesystem: ws: wss: 'unsafe-inline';`
+ let featureDataAttributes = ''
+ try {
+ // Fetch and parse featureConfigs
+ const featureConfigs = JSON.parse(await this.generateFeatureConfigsData())
+ featureDataAttributes = featureConfigs
+ .map((config: FeatureContext[]) => `data-feature-${config[1].name}="${config[1].variation}"`)
+ .join(' ')
+ } catch (error) {
+ // eslint-disable-next-line aws-toolkits/no-console-log
+ console.error('Error setting data-feature attribute for featureConfigs:', error)
+ }
return `
- Amazon Q (Preview)
- ${await this.generateJS(extensionURI, webView)}
+ Amazon Q (Preview)
+ ${await this.generateJS(extensionURI, webView)}
-
+
`
}
@@ -41,29 +66,27 @@ export class WebViewContentGenerator {
? Uri.parse(serverHostname).with({ path: `/${source}` })
: webView.asWebviewUri(javascriptUri)
- const cssEntrypoint = webView.asWebviewUri(
- Uri.joinPath(globals.context.extensionUri, 'resources', 'css', 'amazonq-webview.css')
- )
+ const cssEntrypoints = [
+ Uri.joinPath(globals.context.extensionUri, 'resources', 'css', 'amazonq-webview.css'),
+ Uri.joinPath(globals.context.extensionUri, 'resources', 'css', 'amazonq-chat.css'),
+ ]
- let featureConfigs = new Map()
- try {
- await FeatureConfigProvider.instance.fetchFeatureConfigs()
- featureConfigs = FeatureConfigProvider.getFeatureConfigs()
- } catch (error) {
- // eslint-disable-next-line aws-toolkits/no-console-log
- console.error('Error fetching feature configs:', error)
- }
+ const cssEntrypointsMap = cssEntrypoints.map((item) => webView.asWebviewUri(item))
+ const cssLinks = cssEntrypointsMap.map((uri) => ``).join('\n')
+
+ // Fetch featureConfigs and use it within the script
+ const featureConfigsString = await this.generateFeatureConfigsData()
return `
-
+ ${cssLinks}
+
`
}
}
From 4e51c455ca155036461bfc142f7cfbe52c10b434 Mon Sep 17 00:00:00 2001
From: Nikolas Komonen <118216176+nkomonen-amazon@users.noreply.github.com>
Date: Thu, 17 Oct 2024 15:40:47 -0400
Subject: [PATCH 61/87] fix(release): changelog item (#5808)
Since the amazonq one is empty
---
License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
Signed-off-by: nkomonen-amazon
---
.../Bug Fix-4519935f-b219-4535-8608-da1940a5b2a6.json | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 packages/amazonq/.changes/next-release/Bug Fix-4519935f-b219-4535-8608-da1940a5b2a6.json
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-4519935f-b219-4535-8608-da1940a5b2a6.json b/packages/amazonq/.changes/next-release/Bug Fix-4519935f-b219-4535-8608-da1940a5b2a6.json
new file mode 100644
index 00000000000..58ef86f32d5
--- /dev/null
+++ b/packages/amazonq/.changes/next-release/Bug Fix-4519935f-b219-4535-8608-da1940a5b2a6.json
@@ -0,0 +1,4 @@
+{
+ "type": "Bug Fix",
+ "description": "Various fixes and changes"
+}
From 3b51ec56a677e6102619b2383888d246454cae19 Mon Sep 17 00:00:00 2001
From: aws-toolkit-automation <>
Date: Thu, 17 Oct 2024 19:45:35 +0000
Subject: [PATCH 62/87] Release 1.30.0
---
package-lock.json | 4 ++--
packages/amazonq/.changes/1.30.0.json | 10 ++++++++++
.../Bug Fix-4519935f-b219-4535-8608-da1940a5b2a6.json | 4 ----
packages/amazonq/CHANGELOG.md | 4 ++++
packages/amazonq/package.json | 2 +-
5 files changed, 17 insertions(+), 7 deletions(-)
create mode 100644 packages/amazonq/.changes/1.30.0.json
delete mode 100644 packages/amazonq/.changes/next-release/Bug Fix-4519935f-b219-4535-8608-da1940a5b2a6.json
diff --git a/package-lock.json b/package-lock.json
index a725e954750..5be38d58b96 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -42,7 +42,7 @@
"prettier": "^3.3.3",
"prettier-plugin-sh": "^0.14.0",
"pretty-quick": "^4.0.0",
- "ts-node": "^10.9.1",
+ "ts-node": "^10.9.2",
"typescript": "^5.0.4",
"webpack": "^5.95.0",
"webpack-cli": "^5.1.4",
@@ -19271,7 +19271,7 @@
},
"packages/amazonq": {
"name": "amazon-q-vscode",
- "version": "1.30.0-SNAPSHOT",
+ "version": "1.30.0",
"license": "Apache-2.0",
"dependencies": {
"aws-core-vscode": "file:../core/"
diff --git a/packages/amazonq/.changes/1.30.0.json b/packages/amazonq/.changes/1.30.0.json
new file mode 100644
index 00000000000..070f0f7be5f
--- /dev/null
+++ b/packages/amazonq/.changes/1.30.0.json
@@ -0,0 +1,10 @@
+{
+ "date": "2024-10-17",
+ "version": "1.30.0",
+ "entries": [
+ {
+ "type": "Bug Fix",
+ "description": "Various fixes and changes"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/amazonq/.changes/next-release/Bug Fix-4519935f-b219-4535-8608-da1940a5b2a6.json b/packages/amazonq/.changes/next-release/Bug Fix-4519935f-b219-4535-8608-da1940a5b2a6.json
deleted file mode 100644
index 58ef86f32d5..00000000000
--- a/packages/amazonq/.changes/next-release/Bug Fix-4519935f-b219-4535-8608-da1940a5b2a6.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Bug Fix",
- "description": "Various fixes and changes"
-}
diff --git a/packages/amazonq/CHANGELOG.md b/packages/amazonq/CHANGELOG.md
index fc446c90e9b..7c0f7a7cde8 100644
--- a/packages/amazonq/CHANGELOG.md
+++ b/packages/amazonq/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 1.30.0 2024-10-17
+
+- **Bug Fix** Various fixes and changes
+
## 1.29.0 2024-10-10
- **Bug Fix** Amazon Q /dev: include telemetry for workspace usage when generating new files
diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json
index 38ae92fee85..8a8548c4029 100644
--- a/packages/amazonq/package.json
+++ b/packages/amazonq/package.json
@@ -2,7 +2,7 @@
"name": "amazon-q-vscode",
"displayName": "Amazon Q",
"description": "Amazon Q is your generative AI-powered assistant across the software development lifecycle.",
- "version": "1.30.0-SNAPSHOT",
+ "version": "1.30.0",
"extensionKind": [
"workspace"
],
From 4ca6b1beab2319537319e95f20fd75fb3b651a43 Mon Sep 17 00:00:00 2001
From: aws-toolkit-automation <>
Date: Thu, 17 Oct 2024 20:57:26 +0000
Subject: [PATCH 63/87] Update version to snapshot version: 1.31.0-SNAPSHOT
---
package-lock.json | 4 ++--
packages/amazonq/package.json | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 5be38d58b96..da279e6f337 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -42,7 +42,7 @@
"prettier": "^3.3.3",
"prettier-plugin-sh": "^0.14.0",
"pretty-quick": "^4.0.0",
- "ts-node": "^10.9.2",
+ "ts-node": "^10.9.1",
"typescript": "^5.0.4",
"webpack": "^5.95.0",
"webpack-cli": "^5.1.4",
@@ -19271,7 +19271,7 @@
},
"packages/amazonq": {
"name": "amazon-q-vscode",
- "version": "1.30.0",
+ "version": "1.31.0-SNAPSHOT",
"license": "Apache-2.0",
"dependencies": {
"aws-core-vscode": "file:../core/"
diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json
index 8a8548c4029..eb4034aa1f2 100644
--- a/packages/amazonq/package.json
+++ b/packages/amazonq/package.json
@@ -2,7 +2,7 @@
"name": "amazon-q-vscode",
"displayName": "Amazon Q",
"description": "Amazon Q is your generative AI-powered assistant across the software development lifecycle.",
- "version": "1.30.0",
+ "version": "1.31.0-SNAPSHOT",
"extensionKind": [
"workspace"
],
From 9608507c15bbdf9792c50aa8bee60667236ec65f Mon Sep 17 00:00:00 2001
From: aws-toolkit-automation <>
Date: Thu, 17 Oct 2024 19:45:26 +0000
Subject: [PATCH 64/87] Release 3.29.0
---
package-lock.json | 4 ++--
packages/toolkit/.changes/3.29.0.json | 10 ++++++++++
.../Bug Fix-a0acdaa1-7356-4f58-bc9b-fb7b708fe831.json | 4 ----
packages/toolkit/CHANGELOG.md | 4 ++++
packages/toolkit/package.json | 2 +-
5 files changed, 17 insertions(+), 7 deletions(-)
create mode 100644 packages/toolkit/.changes/3.29.0.json
delete mode 100644 packages/toolkit/.changes/next-release/Bug Fix-a0acdaa1-7356-4f58-bc9b-fb7b708fe831.json
diff --git a/package-lock.json b/package-lock.json
index da279e6f337..1a809d8c2ea 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -42,7 +42,7 @@
"prettier": "^3.3.3",
"prettier-plugin-sh": "^0.14.0",
"pretty-quick": "^4.0.0",
- "ts-node": "^10.9.1",
+ "ts-node": "^10.9.2",
"typescript": "^5.0.4",
"webpack": "^5.95.0",
"webpack-cli": "^5.1.4",
@@ -19432,7 +19432,7 @@
},
"packages/toolkit": {
"name": "aws-toolkit-vscode",
- "version": "3.29.0-SNAPSHOT",
+ "version": "3.29.0",
"license": "Apache-2.0",
"dependencies": {
"aws-core-vscode": "file:../core/"
diff --git a/packages/toolkit/.changes/3.29.0.json b/packages/toolkit/.changes/3.29.0.json
new file mode 100644
index 00000000000..0be09e8c1b8
--- /dev/null
+++ b/packages/toolkit/.changes/3.29.0.json
@@ -0,0 +1,10 @@
+{
+ "date": "2024-10-17",
+ "version": "3.29.0",
+ "entries": [
+ {
+ "type": "Bug Fix",
+ "description": "Fix userCredentialsUtils.test.ts so it won't remove the actual aws config"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/toolkit/.changes/next-release/Bug Fix-a0acdaa1-7356-4f58-bc9b-fb7b708fe831.json b/packages/toolkit/.changes/next-release/Bug Fix-a0acdaa1-7356-4f58-bc9b-fb7b708fe831.json
deleted file mode 100644
index ecefcc6e87b..00000000000
--- a/packages/toolkit/.changes/next-release/Bug Fix-a0acdaa1-7356-4f58-bc9b-fb7b708fe831.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type": "Bug Fix",
- "description": "Fix userCredentialsUtils.test.ts so it won't remove the actual aws config"
-}
diff --git a/packages/toolkit/CHANGELOG.md b/packages/toolkit/CHANGELOG.md
index 4e4b1a64f02..a360d45ce71 100644
--- a/packages/toolkit/CHANGELOG.md
+++ b/packages/toolkit/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 3.29.0 2024-10-17
+
+- **Bug Fix** Fix userCredentialsUtils.test.ts so it won't remove the actual aws config
+
## 3.28.0 2024-10-10
- **Breaking Change** Bumping VS Code minimum version to 1.83.0
diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json
index 84a0b235940..3eed3404a38 100644
--- a/packages/toolkit/package.json
+++ b/packages/toolkit/package.json
@@ -2,7 +2,7 @@
"name": "aws-toolkit-vscode",
"displayName": "AWS Toolkit",
"description": "Including CodeCatalyst, Infrastructure Composer, and support for Lambda, S3, CloudWatch Logs, CloudFormation, and many other services.",
- "version": "3.29.0-SNAPSHOT",
+ "version": "3.29.0",
"extensionKind": [
"workspace"
],
From 1391ad589acd0502fb7bcc3b1fb71a0cc9bba73b Mon Sep 17 00:00:00 2001
From: aws-toolkit-automation <>
Date: Thu, 17 Oct 2024 20:44:08 +0000
Subject: [PATCH 65/87] Update version to snapshot version: 3.30.0-SNAPSHOT
---
package-lock.json | 4 ++--
packages/toolkit/package.json | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 1a809d8c2ea..1c930e50293 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -42,7 +42,7 @@
"prettier": "^3.3.3",
"prettier-plugin-sh": "^0.14.0",
"pretty-quick": "^4.0.0",
- "ts-node": "^10.9.2",
+ "ts-node": "^10.9.1",
"typescript": "^5.0.4",
"webpack": "^5.95.0",
"webpack-cli": "^5.1.4",
@@ -19432,7 +19432,7 @@
},
"packages/toolkit": {
"name": "aws-toolkit-vscode",
- "version": "3.29.0",
+ "version": "3.30.0-SNAPSHOT",
"license": "Apache-2.0",
"dependencies": {
"aws-core-vscode": "file:../core/"
diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json
index 3eed3404a38..ff8c73a4c09 100644
--- a/packages/toolkit/package.json
+++ b/packages/toolkit/package.json
@@ -2,7 +2,7 @@
"name": "aws-toolkit-vscode",
"displayName": "AWS Toolkit",
"description": "Including CodeCatalyst, Infrastructure Composer, and support for Lambda, S3, CloudWatch Logs, CloudFormation, and many other services.",
- "version": "3.29.0",
+ "version": "3.30.0-SNAPSHOT",
"extensionKind": [
"workspace"
],
From 9a6a097a257c4b7f18fef280216a4be8f318bcdc Mon Sep 17 00:00:00 2001
From: Hweinstock <42325418+Hweinstock@users.noreply.github.com>
Date: Thu, 17 Oct 2024 18:06:14 -0400
Subject: [PATCH 66/87] deps: drop fs-extra #5803
## Problem
Follow up from
https://github.com/aws/aws-toolkit-vscode/pull/5761#issuecomment-2417987209.
## Solution
`npm uninstall --save '@types/fs-extra' 'fs-extra' -w packages/core/`
---
package-lock.json | 24 ++----------------------
packages/core/package.json | 2 --
2 files changed, 2 insertions(+), 24 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 1c930e50293..6e407e4e0b5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6943,14 +6943,6 @@
"@types/range-parser": "*"
}
},
- "node_modules/@types/fs-extra": {
- "version": "9.0.13",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
"node_modules/@types/glob": {
"version": "8.1.0",
"dev": true,
@@ -11804,18 +11796,6 @@
"license": "MIT",
"optional": true
},
- "node_modules/fs-extra": {
- "version": "10.1.0",
- "license": "MIT",
- "dependencies": {
- "graceful-fs": "^4.2.0",
- "jsonfile": "^6.0.1",
- "universalify": "^2.0.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/fs-monkey": {
"version": "1.0.3",
"dev": true,
@@ -13396,6 +13376,7 @@
},
"node_modules/jsonfile": {
"version": "6.1.0",
+ "dev": true,
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
@@ -17797,6 +17778,7 @@
},
"node_modules/universalify": {
"version": "2.0.0",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
@@ -19318,7 +19300,6 @@
"cross-fetch": "^4.0.0",
"cross-spawn": "^7.0.3",
"fast-json-patch": "^3.1.1",
- "fs-extra": "^10.0.1",
"glob": "^10.3.10",
"got": "^11.8.5",
"highlight.js": "^11.9.0",
@@ -19360,7 +19341,6 @@
"@types/circular-dependency-plugin": "^5.0.8",
"@types/cross-spawn": "^6.0.6",
"@types/diff": "^5.0.7",
- "@types/fs-extra": "^9.0.11",
"@types/glob": "^8.1.0",
"@types/js-yaml": "^4.0.5",
"@types/jsdom": "^21.1.6",
diff --git a/packages/core/package.json b/packages/core/package.json
index be6f09e5cc8..7c247e3d9e3 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -411,7 +411,6 @@
"@types/circular-dependency-plugin": "^5.0.8",
"@types/cross-spawn": "^6.0.6",
"@types/diff": "^5.0.7",
- "@types/fs-extra": "^9.0.11",
"@types/glob": "^8.1.0",
"@types/js-yaml": "^4.0.5",
"@types/jsdom": "^21.1.6",
@@ -488,7 +487,6 @@
"cross-fetch": "^4.0.0",
"cross-spawn": "^7.0.3",
"fast-json-patch": "^3.1.1",
- "fs-extra": "^10.0.1",
"glob": "^10.3.10",
"got": "^11.8.5",
"highlight.js": "^11.9.0",
From d7dc67eb4e891bdb0416899ac601b0b6e91d47b0 Mon Sep 17 00:00:00 2001
From: Hweinstock <42325418+Hweinstock@users.noreply.github.com>
Date: Thu, 17 Oct 2024 18:11:05 -0400
Subject: [PATCH 67/87] deps: more-precise version of `@types/node` #5801
## Problem
Follow up to:
https://github.com/aws/aws-toolkit-vscode/pull/5761#discussion_r1803806543
## Solution
- target a version of `@types/node` that is closer to the actual version running in CI.
- add a techdebt.test.ts reminder.
---
package-lock.json | 16 ++++++++--------
package.json | 2 +-
packages/core/src/test/techdebt.test.ts | 2 ++
3 files changed, 11 insertions(+), 9 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 6e407e4e0b5..2a25b9529dd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,7 +15,7 @@
"plugins/*"
],
"dependencies": {
- "@types/node": "^22.7.5",
+ "@types/node": "^18.19.55",
"vscode-nls": "^5.2.0",
"vscode-nls-dev": "^4.0.4"
},
@@ -7051,12 +7051,12 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "22.7.5",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
- "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
+ "version": "18.19.55",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.55.tgz",
+ "integrity": "sha512-zzw5Vw52205Zr/nmErSEkN5FLqXPuKX/k5d1D7RKHATGqU7y6YfX9QxZraUzUrFGqH6XzOzG196BC35ltJC4Cw==",
"license": "MIT",
"dependencies": {
- "undici-types": "~6.19.2"
+ "undici-types": "~5.26.4"
}
},
"node_modules/@types/node-fetch": {
@@ -17767,9 +17767,9 @@
"license": "MIT"
},
"node_modules/undici-types": {
- "version": "6.19.8",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
- "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"license": "MIT"
},
"node_modules/unescape-html": {
diff --git a/package.json b/package.json
index 3e8821b017c..39f4fe478eb 100644
--- a/package.json
+++ b/package.json
@@ -69,7 +69,7 @@
"webpack-merge": "^5.10.0"
},
"dependencies": {
- "@types/node": "^22.7.5",
+ "@types/node": "^18.19.55",
"vscode-nls": "^5.2.0",
"vscode-nls-dev": "^4.0.4"
}
diff --git a/packages/core/src/test/techdebt.test.ts b/packages/core/src/test/techdebt.test.ts
index 43a2d180f59..7c3a8ad1436 100644
--- a/packages/core/src/test/techdebt.test.ts
+++ b/packages/core/src/test/techdebt.test.ts
@@ -35,6 +35,8 @@ describe('tech debt', function () {
semver.lt(minNodejs, '18.0.0'),
'with node16+, we can now use AbortController to cancel Node things (child processes, HTTP requests, etc.)'
)
+ // This is relevant for the use of `fs.cpSync` in the copyFiles scripts.
+ assert.ok(semver.lt(minNodejs, '18.0.0'), 'with node18+, we can remove the dependency on @types/node@18')
})
it('remove separate sessions login edge cases', async function () {
From 751d87c40dfbb5dc98f872fcb745903d45ff3ee4 Mon Sep 17 00:00:00 2001
From: Hweinstock <42325418+Hweinstock@users.noreply.github.com>
Date: Fri, 18 Oct 2024 16:43:59 -0400
Subject: [PATCH 68/87] feat(ec2): provide link for customer to view system
logs (#5633)
## Problem
Limited ability to diagnose problems with EC2 instances provided in the
toolkit.
## Solution
Add a right click option to provide system log of Ec2 instance in
read-only file view.
### Implementation Details
- Log view does NOT live update. This would require refactoring the CWL
significantly. Better to get the feature out and see if this is
something customers want/need.
- To reduce code duplication two new utility components were added
`decodeBase64` in`core/src/shared/utilities/textUtilities.ts` and
`UriSchema` in `core/src/shared/utilities/uriUtils.ts`.
- `decodeBase64` is a helper function to wrap the use of `Buffer.from(X,
"base64").toString()` throughout the toolkit.
- `UriSchema` provides a general framework for translating between an
object and an URI, and vice versa. It allows us to avoid writing the
`isValid` method in all cases. With more work this could likely be more
general, but is only used twice so unclear if it is worth pursuing.
- These changes involved changing some function calls, especially in the
CWL code, but does not impact functionality.
### Pictures
---
License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---------
Co-authored-by: Justin M. Keyes
---
packages/core/package.nls.json | 1 +
.../cloudWatchLogs/changeLogSearch.ts | 4 +-
.../cloudWatchLogs/cloudWatchLogsUtils.ts | 39 ++++++-----------
.../commands/copyLogResource.ts | 4 +-
.../commands/saveCurrentLogDataContent.ts | 4 +-
.../cloudWatchLogs/commands/searchLogGroup.ts | 11 +++--
.../cloudWatchLogs/commands/viewLogStream.ts | 4 +-
.../document/logDataDocumentProvider.ts | 4 +-
.../document/logStreamsCodeLensProvider.ts | 15 ++++---
.../registry/logDataRegistry.ts | 9 +++-
.../core/src/awsService/ec2/activation.ts | 10 +++++
packages/core/src/awsService/ec2/commands.ts | 7 +++-
.../awsService/ec2/ec2LogDocumentProvider.ts | 42 +++++++++++++++++++
.../lambda/vue/remoteInvoke/invokeLambda.ts | 3 +-
packages/core/src/shared/clients/ec2Client.ts | 16 +++++++
packages/core/src/shared/constants.ts | 1 +
.../shared/utilities/textDocumentUtilities.ts | 6 +++
.../src/shared/utilities/textUtilities.ts | 3 ++
.../core/src/shared/utilities/uriUtils.ts | 21 ++++++++++
.../commands/copyLogResource.test.ts | 6 +--
.../saveCurrentLogDataContent.test.ts | 4 +-
.../document/logDataDocumentProvider.test.ts | 11 +++--
.../logStreamsCodeLensProvider.test.ts | 4 +-
.../registry/logDataRegistry.test.ts | 24 ++++++++---
.../awsService/cloudWatchLogs/utils.test.ts | 25 ++++++-----
.../ec2/ec2LogDocumentProvider.test.ts | 39 +++++++++++++++++
packages/core/src/test/setupUtil.ts | 3 +-
...-754ec8b7-71ed-4be0-8f07-5eedd1e0fa12.json | 4 ++
28 files changed, 238 insertions(+), 86 deletions(-)
create mode 100644 packages/core/src/awsService/ec2/ec2LogDocumentProvider.ts
create mode 100644 packages/core/src/test/awsService/ec2/ec2LogDocumentProvider.test.ts
create mode 100644 packages/toolkit/.changes/next-release/Feature-754ec8b7-71ed-4be0-8f07-5eedd1e0fa12.json
diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json
index c2d3d644214..d108766cd63 100644
--- a/packages/core/package.nls.json
+++ b/packages/core/package.nls.json
@@ -129,6 +129,7 @@
"AWS.command.ec2.stopInstance": "Stop EC2 Instance",
"AWS.command.ec2.rebootInstance": "Reboot EC2 Instance",
"AWS.command.ec2.copyInstanceId": "Copy Instance Id",
+ "AWS.command.ec2.viewLogs": "View EC2 Logs",
"AWS.command.ecr.copyTagUri": "Copy Tag URI",
"AWS.command.ecr.copyRepositoryUri": "Copy Repository URI",
"AWS.command.ecr.createRepository": "Create Repository...",
diff --git a/packages/core/src/awsService/cloudWatchLogs/changeLogSearch.ts b/packages/core/src/awsService/cloudWatchLogs/changeLogSearch.ts
index 4797e06fc03..349f7339e87 100644
--- a/packages/core/src/awsService/cloudWatchLogs/changeLogSearch.ts
+++ b/packages/core/src/awsService/cloudWatchLogs/changeLogSearch.ts
@@ -5,7 +5,7 @@
import { CancellationError } from '../../shared/utilities/timeoutUtils'
import { telemetry } from '../../shared/telemetry/telemetry'
import { showInputBox } from '../../shared/ui/inputPrompter'
-import { createURIFromArgs, isLogStreamUri, recordTelemetryFilter } from './cloudWatchLogsUtils'
+import { cwlUriSchema, isLogStreamUri, recordTelemetryFilter } from './cloudWatchLogsUtils'
import { prepareDocument } from './commands/searchLogGroup'
import { getActiveDocumentUri } from './document/logDataDocumentProvider'
import { CloudWatchLogsData, filterLogEventsFromUri, LogDataRegistry } from './registry/logDataRegistry'
@@ -98,7 +98,7 @@ export async function changeLogSearchParams(
throw new CancellationError('user')
}
- const newUri = createURIFromArgs(newData.logGroupInfo, newData.parameters)
+ const newUri = cwlUriSchema.form({ logGroupInfo: newData.logGroupInfo, parameters: newData.parameters })
await prepareDocument(newUri, newData, registry)
})
}
diff --git a/packages/core/src/awsService/cloudWatchLogs/cloudWatchLogsUtils.ts b/packages/core/src/awsService/cloudWatchLogs/cloudWatchLogsUtils.ts
index b8c769e92f8..e2356595153 100644
--- a/packages/core/src/awsService/cloudWatchLogs/cloudWatchLogsUtils.ts
+++ b/packages/core/src/awsService/cloudWatchLogs/cloudWatchLogsUtils.ts
@@ -6,8 +6,9 @@ import { telemetry } from '../../shared/telemetry/telemetry'
import * as vscode from 'vscode'
import { CLOUDWATCH_LOGS_SCHEME } from '../../shared/constants'
import { fromExtensionManifest } from '../../shared/settings'
-import { CloudWatchLogsData, CloudWatchLogsGroupInfo } from './registry/logDataRegistry'
+import { CloudWatchLogsArgs, CloudWatchLogsData, CloudWatchLogsGroupInfo } from './registry/logDataRegistry'
import { CloudWatchLogsParameters } from './registry/logDataRegistry'
+import { UriSchema } from '../../shared/utilities/uriUtils'
// URIs are the only vehicle for delivering information to a TextDocumentContentProvider.
// The following functions are used to structure and destructure relevant information to/from a URI.
@@ -32,8 +33,7 @@ export function recordTelemetryFilter(logData: CloudWatchLogsData): void {
export function uriToKey(uri: vscode.Uri): string {
if (uri.query) {
try {
- const { filterPattern, startTime, endTime, limit, streamNameOptions } =
- parseCloudWatchLogsUri(uri).parameters
+ const { filterPattern, startTime, endTime, limit, streamNameOptions } = cwlUriSchema.parse(uri).parameters
const parts = [uri.path, filterPattern, startTime, endTime, limit, streamNameOptions]
return parts.map((p) => p ?? '').join(':')
} catch {
@@ -52,7 +52,7 @@ export function uriToKey(uri: vscode.Uri): string {
* message as the actual log group search. That results in a more fluid UX.
*/
export function msgKey(logGroupInfo: CloudWatchLogsGroupInfo): string {
- const uri = createURIFromArgs(logGroupInfo, {})
+ const uri = cwlUriSchema.form({ logGroupInfo: logGroupInfo, parameters: {} })
return uri.toString()
}
@@ -60,10 +60,7 @@ export function msgKey(logGroupInfo: CloudWatchLogsGroupInfo): string {
* Destructures an awsCloudWatchLogs URI into its component pieces.
* @param uri URI for a Cloudwatch Logs file
*/
-export function parseCloudWatchLogsUri(uri: vscode.Uri): {
- logGroupInfo: CloudWatchLogsGroupInfo
- parameters: CloudWatchLogsParameters
-} {
+function parseCloudWatchLogsUri(uri: vscode.Uri): CloudWatchLogsArgs {
const parts = uri.path.split(':')
if (uri.scheme !== CLOUDWATCH_LOGS_SCHEME) {
@@ -85,18 +82,8 @@ export function parseCloudWatchLogsUri(uri: vscode.Uri): {
}
}
-/** True if given URI is a valid Cloud Watch Logs Uri */
-export function isCwlUri(uri: vscode.Uri): boolean {
- try {
- parseCloudWatchLogsUri(uri)
- return true
- } catch {
- return false
- }
-}
-
export function patternFromCwlUri(uri: vscode.Uri): CloudWatchLogsParameters['filterPattern'] {
- return parseCloudWatchLogsUri(uri).parameters.filterPattern
+ return cwlUriSchema.parse(uri).parameters.filterPattern
}
/**
@@ -105,7 +92,7 @@ export function patternFromCwlUri(uri: vscode.Uri): CloudWatchLogsParameters['fi
* @returns
*/
export function isLogStreamUri(uri: vscode.Uri): boolean {
- const logGroupInfo = parseCloudWatchLogsUri(uri).logGroupInfo
+ const logGroupInfo = cwlUriSchema.parse(uri).logGroupInfo
return logGroupInfo.streamName !== undefined
}
@@ -115,15 +102,13 @@ export function isLogStreamUri(uri: vscode.Uri): boolean {
* @param streamName Log stream name
* @param regionName AWS region
*/
-export function createURIFromArgs(
- logGroupInfo: CloudWatchLogsGroupInfo,
- parameters: CloudWatchLogsParameters
-): vscode.Uri {
- let uriStr = `${CLOUDWATCH_LOGS_SCHEME}:${logGroupInfo.regionName}:${logGroupInfo.groupName}`
- uriStr += logGroupInfo.streamName ? `:${logGroupInfo.streamName}` : ''
+function createURIFromArgs(args: CloudWatchLogsArgs): vscode.Uri {
+ let uriStr = `${CLOUDWATCH_LOGS_SCHEME}:${args.logGroupInfo.regionName}:${args.logGroupInfo.groupName}`
+ uriStr += args.logGroupInfo.streamName ? `:${args.logGroupInfo.streamName}` : ''
- uriStr += `?${encodeURIComponent(JSON.stringify(parameters))}`
+ uriStr += `?${encodeURIComponent(JSON.stringify(args.parameters))}`
return vscode.Uri.parse(uriStr)
}
+export const cwlUriSchema = new UriSchema(parseCloudWatchLogsUri, createURIFromArgs)
export class CloudWatchLogsSettings extends fromExtensionManifest('aws.cwl', { limit: Number }) {}
diff --git a/packages/core/src/awsService/cloudWatchLogs/commands/copyLogResource.ts b/packages/core/src/awsService/cloudWatchLogs/commands/copyLogResource.ts
index b7cca40c752..846f87e797e 100644
--- a/packages/core/src/awsService/cloudWatchLogs/commands/copyLogResource.ts
+++ b/packages/core/src/awsService/cloudWatchLogs/commands/copyLogResource.ts
@@ -7,7 +7,7 @@ import * as nls from 'vscode-nls'
const localize = nls.loadMessageBundle()
import * as vscode from 'vscode'
-import { isLogStreamUri, parseCloudWatchLogsUri } from '../cloudWatchLogsUtils'
+import { cwlUriSchema, isLogStreamUri } from '../cloudWatchLogsUtils'
import { copyToClipboard } from '../../../shared/utilities/messages'
export async function copyLogResource(uri?: vscode.Uri): Promise {
@@ -20,7 +20,7 @@ export async function copyLogResource(uri?: vscode.Uri): Promise {
throw new Error('no active text editor, or undefined URI')
}
}
- const parsedUri = parseCloudWatchLogsUri(uri)
+ const parsedUri = cwlUriSchema.parse(uri)
const resourceName = isLogStreamUri(uri) ? parsedUri.logGroupInfo.streamName : parsedUri.logGroupInfo.groupName
if (!resourceName) {
diff --git a/packages/core/src/awsService/cloudWatchLogs/commands/saveCurrentLogDataContent.ts b/packages/core/src/awsService/cloudWatchLogs/commands/saveCurrentLogDataContent.ts
index 2e83e04cf76..9d1600fafba 100644
--- a/packages/core/src/awsService/cloudWatchLogs/commands/saveCurrentLogDataContent.ts
+++ b/packages/core/src/awsService/cloudWatchLogs/commands/saveCurrentLogDataContent.ts
@@ -7,7 +7,7 @@ import * as vscode from 'vscode'
import * as nls from 'vscode-nls'
const localize = nls.loadMessageBundle()
-import { isLogStreamUri, parseCloudWatchLogsUri } from '../cloudWatchLogsUtils'
+import { cwlUriSchema, isLogStreamUri } from '../cloudWatchLogsUtils'
import { telemetry, CloudWatchResourceType, Result } from '../../../shared/telemetry/telemetry'
import fs from '../../../shared/fs/fs'
@@ -28,7 +28,7 @@ export async function saveCurrentLogDataContent(): Promise {
const workspaceDir = vscode.workspace.workspaceFolders
? vscode.workspace.workspaceFolders[0].uri
: vscode.Uri.file(fs.getUserHomeDir())
- const uriComponents = parseCloudWatchLogsUri(uri)
+ const uriComponents = cwlUriSchema.parse(uri)
const logGroupInfo = uriComponents.logGroupInfo
const localizedLogFile = localize('AWS.command.saveCurrentLogDataContent.logfile', 'Log File')
diff --git a/packages/core/src/awsService/cloudWatchLogs/commands/searchLogGroup.ts b/packages/core/src/awsService/cloudWatchLogs/commands/searchLogGroup.ts
index 0dfc491bbb4..41ce8d7922f 100644
--- a/packages/core/src/awsService/cloudWatchLogs/commands/searchLogGroup.ts
+++ b/packages/core/src/awsService/cloudWatchLogs/commands/searchLogGroup.ts
@@ -16,7 +16,7 @@ import {
} from '../registry/logDataRegistry'
import { DataQuickPickItem } from '../../../shared/ui/pickerPrompter'
import { isValidResponse, isWizardControl, Wizard, WIZARD_RETRY } from '../../../shared/wizards/wizard'
-import { createURIFromArgs, msgKey, parseCloudWatchLogsUri, recordTelemetryFilter } from '../cloudWatchLogsUtils'
+import { cwlUriSchema, msgKey, recordTelemetryFilter } from '../cloudWatchLogsUtils'
import { DefaultCloudWatchLogsClient } from '../../../shared/clients/cloudWatchLogsClient'
import { CancellationError } from '../../../shared/utilities/timeoutUtils'
import { getLogger } from '../../../shared/logger'
@@ -29,6 +29,7 @@ import { createBackButton, createExitButton, createHelpButton } from '../../../s
import { PromptResult } from '../../../shared/ui/prompter'
import { ToolkitError } from '../../../shared/errors'
import { Messages } from '../../../shared/utilities/messages'
+import { showFile } from '../../../shared/utilities/textDocumentUtilities'
const localize = nls.loadMessageBundle()
@@ -65,9 +66,7 @@ export async function prepareDocument(uri: vscode.Uri, logData: CloudWatchLogsDa
try {
// Gets the data: calls filterLogEventsFromUri().
await registry.fetchNextLogEvents(uri)
- const doc = await vscode.workspace.openTextDocument(uri)
- await vscode.window.showTextDocument(doc, { preview: false })
- await vscode.languages.setTextDocumentLanguage(doc, 'log')
+ await showFile(uri)
} catch (err) {
if (CancellationError.isUserCancelled(err)) {
throw err
@@ -78,7 +77,7 @@ export async function prepareDocument(uri: vscode.Uri, logData: CloudWatchLogsDa
localize(
'AWS.cwl.searchLogGroup.errorRetrievingLogs',
'Failed to get logs for {0}',
- parseCloudWatchLogsUri(uri).logGroupInfo.groupName
+ cwlUriSchema.parse(uri).logGroupInfo.groupName
)
)
}
@@ -105,7 +104,7 @@ export async function searchLogGroup(
}
const userResponse = handleWizardResponse(response, registry)
- const uri = createURIFromArgs(userResponse.logGroupInfo, userResponse.parameters)
+ const uri = cwlUriSchema.form({ logGroupInfo: userResponse.logGroupInfo, parameters: userResponse.parameters })
await prepareDocument(uri, userResponse, registry)
})
}
diff --git a/packages/core/src/awsService/cloudWatchLogs/commands/viewLogStream.ts b/packages/core/src/awsService/cloudWatchLogs/commands/viewLogStream.ts
index a8ba6dbba76..2bfafd6d2d7 100644
--- a/packages/core/src/awsService/cloudWatchLogs/commands/viewLogStream.ts
+++ b/packages/core/src/awsService/cloudWatchLogs/commands/viewLogStream.ts
@@ -21,11 +21,11 @@ import {
initLogData as initLogData,
filterLogEventsFromUri,
} from '../registry/logDataRegistry'
-import { createURIFromArgs } from '../cloudWatchLogsUtils'
import { prepareDocument, searchLogGroup } from './searchLogGroup'
import { telemetry, Result } from '../../../shared/telemetry/telemetry'
import { CancellationError } from '../../../shared/utilities/timeoutUtils'
import { formatLocalized } from '../../../shared/utilities/textUtilities'
+import { cwlUriSchema } from '../cloudWatchLogsUtils'
export async function viewLogStream(node: LogGroupNode, registry: LogDataRegistry): Promise {
await telemetry.cloudwatchlogs_open.run(async (span) => {
@@ -52,7 +52,7 @@ export async function viewLogStream(node: LogGroupNode, registry: LogDataRegistr
limit: registry.configuration.get('limit', 10000),
}
- const uri = createURIFromArgs(logGroupInfo, parameters)
+ const uri = cwlUriSchema.form({ logGroupInfo: logGroupInfo, parameters: parameters })
const logData = initLogData(logGroupInfo, parameters, filterLogEventsFromUri)
await prepareDocument(uri, logData, registry)
})
diff --git a/packages/core/src/awsService/cloudWatchLogs/document/logDataDocumentProvider.ts b/packages/core/src/awsService/cloudWatchLogs/document/logDataDocumentProvider.ts
index 8eb390f7760..5925911893b 100644
--- a/packages/core/src/awsService/cloudWatchLogs/document/logDataDocumentProvider.ts
+++ b/packages/core/src/awsService/cloudWatchLogs/document/logDataDocumentProvider.ts
@@ -5,8 +5,8 @@
import * as vscode from 'vscode'
import { CloudWatchLogsGroupInfo, LogDataRegistry, UriString } from '../registry/logDataRegistry'
import { getLogger } from '../../../shared/logger'
-import { isCwlUri } from '../cloudWatchLogsUtils'
import { generateTextFromLogEvents, LineToLogStreamMap } from './textContent'
+import { cwlUriSchema } from '../cloudWatchLogsUtils'
export class LogDataDocumentProvider implements vscode.TextDocumentContentProvider {
/** Resolves the correct {@link LineToLogStreamMap} instance for a given URI */
@@ -26,7 +26,7 @@ export class LogDataDocumentProvider implements vscode.TextDocumentContentProvid
}
public provideTextDocumentContent(uri: vscode.Uri): string {
- if (!isCwlUri(uri)) {
+ if (!cwlUriSchema.isValid(uri)) {
throw new Error(`Uri is not a CWL Uri, so no text can be provided: ${uri.toString()}`)
}
const events = this.registry.fetchCachedLogEvents(uri)
diff --git a/packages/core/src/awsService/cloudWatchLogs/document/logStreamsCodeLensProvider.ts b/packages/core/src/awsService/cloudWatchLogs/document/logStreamsCodeLensProvider.ts
index 5c83ab929d9..9870ff009d2 100644
--- a/packages/core/src/awsService/cloudWatchLogs/document/logStreamsCodeLensProvider.ts
+++ b/packages/core/src/awsService/cloudWatchLogs/document/logStreamsCodeLensProvider.ts
@@ -6,12 +6,7 @@
import * as vscode from 'vscode'
import { CLOUDWATCH_LOGS_SCHEME } from '../../../shared/constants'
import { CloudWatchLogsGroupInfo, LogDataRegistry } from '../registry/logDataRegistry'
-import {
- CloudWatchLogsSettings,
- createURIFromArgs,
- isLogStreamUri,
- parseCloudWatchLogsUri,
-} from '../cloudWatchLogsUtils'
+import { CloudWatchLogsSettings, cwlUriSchema, isLogStreamUri } from '../cloudWatchLogsUtils'
import { LogDataDocumentProvider } from './logDataDocumentProvider'
type IdWithLine = { streamId: string; lineNum: number }
@@ -43,7 +38,7 @@ export class LogStreamCodeLensProvider implements vscode.CodeLensProvider {
return []
}
- const logGroupInfo = parseCloudWatchLogsUri(uri).logGroupInfo
+ const logGroupInfo = cwlUriSchema.parse(uri).logGroupInfo
if (logGroupInfo.streamName) {
// This means we have a stream file not a log search.
@@ -64,7 +59,11 @@ export class LogStreamCodeLensProvider implements vscode.CodeLensProvider {
createLogStreamCodeLens(logGroupInfo: CloudWatchLogsGroupInfo, idWithLine: IdWithLine): vscode.CodeLens {
const settings = new CloudWatchLogsSettings()
const limit = settings.get('limit', 1000)
- const streamUri = createURIFromArgs({ ...logGroupInfo, streamName: idWithLine.streamId }, { limit: limit })
+ const cwlArgs = {
+ logGroupInfo: { ...logGroupInfo, streamName: idWithLine.streamId },
+ parameters: { limit: limit },
+ }
+ const streamUri = cwlUriSchema.form(cwlArgs)
const cmd: vscode.Command = {
command: 'aws.loadLogStreamFile',
arguments: [streamUri, this.registry],
diff --git a/packages/core/src/awsService/cloudWatchLogs/registry/logDataRegistry.ts b/packages/core/src/awsService/cloudWatchLogs/registry/logDataRegistry.ts
index e8ae51558e5..88945d61bd2 100644
--- a/packages/core/src/awsService/cloudWatchLogs/registry/logDataRegistry.ts
+++ b/packages/core/src/awsService/cloudWatchLogs/registry/logDataRegistry.ts
@@ -5,7 +5,7 @@
import * as vscode from 'vscode'
import { CloudWatchLogs } from 'aws-sdk'
-import { CloudWatchLogsSettings, parseCloudWatchLogsUri, uriToKey, msgKey } from '../cloudWatchLogsUtils'
+import { CloudWatchLogsSettings, uriToKey, msgKey, cwlUriSchema } from '../cloudWatchLogsUtils'
import { DefaultCloudWatchLogsClient } from '../../../shared/clients/cloudWatchLogsClient'
import { waitTimeout } from '../../../shared/utilities/timeoutUtils'
import { Messages } from '../../../shared/utilities/messages'
@@ -190,7 +190,7 @@ export class LogDataRegistry {
if (this.isRegistered(uri)) {
throw new Error(`Already registered: ${uri.toString()}`)
}
- const data = parseCloudWatchLogsUri(uri)
+ const data = cwlUriSchema.parse(uri)
this.setLogData(uri, initLogData(data.logGroupInfo, data.parameters, retrieveLogsFunction))
}
@@ -281,6 +281,11 @@ export function initLogData(
}
}
+export type CloudWatchLogsArgs = {
+ logGroupInfo: CloudWatchLogsGroupInfo
+ parameters: CloudWatchLogsParameters
+}
+
export type CloudWatchLogsGroupInfo = {
groupName: string
regionName: string
diff --git a/packages/core/src/awsService/ec2/activation.ts b/packages/core/src/awsService/ec2/activation.ts
index 550a0bbf47c..45d0a5c369e 100644
--- a/packages/core/src/awsService/ec2/activation.ts
+++ b/packages/core/src/awsService/ec2/activation.ts
@@ -2,6 +2,7 @@
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
+import * as vscode from 'vscode'
import { ExtContext } from '../../shared/extensions'
import { Commands } from '../../shared/vscode/commands2'
import { telemetry } from '../../shared/telemetry/telemetry'
@@ -15,10 +16,16 @@ import {
startInstance,
stopInstance,
refreshExplorer,
+ openLogDocument,
linkToLaunchInstance,
} from './commands'
+import { ec2LogsScheme } from '../../shared/constants'
+import { Ec2LogDocumentProvider } from './ec2LogDocumentProvider'
export async function activate(ctx: ExtContext): Promise {
+ ctx.extensionContext.subscriptions.push(
+ vscode.workspace.registerTextDocumentContentProvider(ec2LogsScheme, new Ec2LogDocumentProvider())
+ )
ctx.extensionContext.subscriptions.push(
Commands.register('aws.ec2.openTerminal', async (node?: Ec2InstanceNode) => {
await telemetry.ec2_connectToInstance.run(async (span) => {
@@ -30,6 +37,9 @@ export async function activate(ctx: ExtContext): Promise {
Commands.register('aws.ec2.copyInstanceId', async (node: Ec2InstanceNode) => {
await copyTextCommand(node, 'id')
}),
+ Commands.register('aws.ec2.viewLogs', async (node?: Ec2InstanceNode) => {
+ await openLogDocument(node)
+ }),
Commands.register('aws.ec2.openRemoteConnection', async (node?: Ec2Node) => {
await openRemoteConnection(node)
diff --git a/packages/core/src/awsService/ec2/commands.ts b/packages/core/src/awsService/ec2/commands.ts
index f09ea0ea10c..01cdb5b5f53 100644
--- a/packages/core/src/awsService/ec2/commands.ts
+++ b/packages/core/src/awsService/ec2/commands.ts
@@ -2,7 +2,6 @@
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
-
import { Ec2InstanceNode } from './explorer/ec2InstanceNode'
import { Ec2Node } from './explorer/ec2ParentNode'
import { Ec2ConnectionManager } from './model'
@@ -10,9 +9,11 @@ import { Ec2Prompter, instanceFilter, Ec2Selection } from './prompter'
import { SafeEc2Instance, Ec2Client } from '../../shared/clients/ec2Client'
import { copyToClipboard } from '../../shared/utilities/messages'
import { getLogger } from '../../shared/logger'
+import { ec2LogSchema } from './ec2LogDocumentProvider'
import { getAwsConsoleUrl } from '../../shared/awsConsole'
import { showRegionPrompter } from '../../auth/utils'
import { openUrl } from '../../shared/utilities/vsCodeUtils'
+import { showFile } from '../../shared/utilities/textDocumentUtilities'
export function refreshExplorer(node?: Ec2Node) {
if (node) {
@@ -71,3 +72,7 @@ async function getSelection(node?: Ec2Node, filter?: instanceFilter): Promise {
await copyToClipboard(instanceId, 'Id')
}
+
+export async function openLogDocument(node?: Ec2InstanceNode): Promise {
+ return await showFile(ec2LogSchema.form(await getSelection(node)))
+}
diff --git a/packages/core/src/awsService/ec2/ec2LogDocumentProvider.ts b/packages/core/src/awsService/ec2/ec2LogDocumentProvider.ts
new file mode 100644
index 00000000000..1c8e68f0e02
--- /dev/null
+++ b/packages/core/src/awsService/ec2/ec2LogDocumentProvider.ts
@@ -0,0 +1,42 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import * as vscode from 'vscode'
+import { Ec2Selection } from './prompter'
+import { Ec2Client } from '../../shared/clients/ec2Client'
+import { ec2LogsScheme } from '../../shared/constants'
+import { UriSchema } from '../../shared/utilities/uriUtils'
+
+export class Ec2LogDocumentProvider implements vscode.TextDocumentContentProvider {
+ public constructor() {}
+
+ public async provideTextDocumentContent(uri: vscode.Uri): Promise {
+ if (!ec2LogSchema.isValid(uri)) {
+ throw new Error(`Invalid EC2 Logs URI: ${uri.toString()}`)
+ }
+ const ec2Selection = ec2LogSchema.parse(uri)
+ const ec2Client = new Ec2Client(ec2Selection.region)
+ const consoleOutput = await ec2Client.getConsoleOutput(ec2Selection.instanceId, false)
+ return consoleOutput.Output
+ }
+}
+
+export const ec2LogSchema = new UriSchema(parseEc2Uri, formEc2Uri)
+
+function parseEc2Uri(uri: vscode.Uri): Ec2Selection {
+ const parts = uri.path.split(':')
+
+ if (uri.scheme !== ec2LogsScheme) {
+ throw new Error(`URI ${uri} is not parseable for EC2 Logs`)
+ }
+
+ return {
+ instanceId: parts[1],
+ region: parts[0],
+ }
+}
+
+function formEc2Uri(selection: Ec2Selection): vscode.Uri {
+ return vscode.Uri.parse(`${ec2LogsScheme}:${selection.region}:${selection.instanceId}`)
+}
diff --git a/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts b/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts
index a644b3b912f..b75dd9477ba 100644
--- a/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts
+++ b/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts
@@ -20,6 +20,7 @@ import * as nls from 'vscode-nls'
import { VueWebview } from '../../../webviews/main'
import { telemetry } from '../../../shared/telemetry/telemetry'
import { Result } from '../../../shared/telemetry/telemetry'
+import { decodeBase64 } from '../../../shared'
const localize = nls.loadMessageBundle()
@@ -61,7 +62,7 @@ export class RemoteInvokeWebview extends VueWebview {
try {
const funcResponse = await this.client.invoke(this.data.FunctionArn, input)
- const logs = funcResponse.LogResult ? Buffer.from(funcResponse.LogResult, 'base64').toString() : ''
+ const logs = funcResponse.LogResult ? decodeBase64(funcResponse.LogResult) : ''
const payload = funcResponse.Payload ? funcResponse.Payload : JSON.stringify({})
this.channel.appendLine(`Invocation result for ${this.data.FunctionArn}`)
diff --git a/packages/core/src/shared/clients/ec2Client.ts b/packages/core/src/shared/clients/ec2Client.ts
index 49ff4841e58..490da6dda3f 100644
--- a/packages/core/src/shared/clients/ec2Client.ts
+++ b/packages/core/src/shared/clients/ec2Client.ts
@@ -12,6 +12,7 @@ import { PromiseResult } from 'aws-sdk/lib/request'
import { Timeout } from '../utilities/timeoutUtils'
import { showMessageWithCancel } from '../utilities/messages'
import { ToolkitError, isAwsError } from '../errors'
+import { decodeBase64 } from '../utilities/textUtilities'
/**
* A wrapper around EC2.Instance where we can safely assume InstanceId field exists.
@@ -22,6 +23,11 @@ export interface SafeEc2Instance extends EC2.Instance {
LastSeenStatus: EC2.InstanceStateName
}
+interface SafeEc2GetConsoleOutputResult extends EC2.GetConsoleOutputRequest {
+ Output: string
+ InstanceId: string
+}
+
export class Ec2Client {
public constructor(public readonly regionCode: string) {}
@@ -229,6 +235,16 @@ export class Ec2Client {
const association = await this.getIamInstanceProfileAssociation(instanceId)
return association ? association.IamInstanceProfile : undefined
}
+
+ public async getConsoleOutput(instanceId: string, latest: boolean): Promise {
+ const client = await this.createSdkClient()
+ const response = await client.getConsoleOutput({ InstanceId: instanceId, Latest: latest }).promise()
+ return {
+ ...response,
+ InstanceId: instanceId,
+ Output: response.Output ? decodeBase64(response.Output) : '',
+ }
+ }
}
export function getNameOfInstance(instance: EC2.Instance): string | undefined {
diff --git a/packages/core/src/shared/constants.ts b/packages/core/src/shared/constants.ts
index 908624383dc..2a7a67355b5 100644
--- a/packages/core/src/shared/constants.ts
+++ b/packages/core/src/shared/constants.ts
@@ -129,6 +129,7 @@ export const ecsIamPermissionsUrl = vscode.Uri.parse(
*/
export const CLOUDWATCH_LOGS_SCHEME = 'aws-cwl' // eslint-disable-line @typescript-eslint/naming-convention
export const AWS_SCHEME = 'aws' // eslint-disable-line @typescript-eslint/naming-convention
+export const ec2LogsScheme = 'aws-ec2'
export const amazonQDiffScheme = 'amazon-q-diff'
export const lambdaPackageTypeImage = 'Image'
diff --git a/packages/core/src/shared/utilities/textDocumentUtilities.ts b/packages/core/src/shared/utilities/textDocumentUtilities.ts
index 71114bd2389..5a47e231b80 100644
--- a/packages/core/src/shared/utilities/textDocumentUtilities.ts
+++ b/packages/core/src/shared/utilities/textDocumentUtilities.ts
@@ -184,3 +184,9 @@ export function getIndentedCode(message: any, doc: vscode.TextDocument, selectio
return indent(message.code, indentation.length)
}
+
+export async function showFile(uri: vscode.Uri) {
+ const doc = await vscode.workspace.openTextDocument(uri)
+ await vscode.window.showTextDocument(doc, { preview: false })
+ await vscode.languages.setTextDocumentLanguage(doc, 'log')
+}
diff --git a/packages/core/src/shared/utilities/textUtilities.ts b/packages/core/src/shared/utilities/textUtilities.ts
index f337bb31276..46423288645 100644
--- a/packages/core/src/shared/utilities/textUtilities.ts
+++ b/packages/core/src/shared/utilities/textUtilities.ts
@@ -406,6 +406,9 @@ export function undefinedIfEmpty(str: string | undefined): string | undefined {
return undefined
}
+export function decodeBase64(base64Str: string): string {
+ return Buffer.from(base64Str, 'base64').toString()
+}
/**
* Extracts the file path and selection context from the message.
*
diff --git a/packages/core/src/shared/utilities/uriUtils.ts b/packages/core/src/shared/utilities/uriUtils.ts
index edf747a4df4..deca450570b 100644
--- a/packages/core/src/shared/utilities/uriUtils.ts
+++ b/packages/core/src/shared/utilities/uriUtils.ts
@@ -26,6 +26,27 @@ export function fromQueryToParameters(query: vscode.Uri['query']): Map {
+ public constructor(
+ public parse: (uri: vscode.Uri) => T,
+ public form: (obj: T) => vscode.Uri
+ ) {}
+
+ public isValid(uri: vscode.Uri): boolean {
+ try {
+ this.parse(uri)
+ return true
+ } catch (e) {
+ return false
+ }
+ }
+}
+
/**
* Converts a string path to a Uri, or returns the given Uri if it is already a Uri.
*
diff --git a/packages/core/src/test/awsService/cloudWatchLogs/commands/copyLogResource.test.ts b/packages/core/src/test/awsService/cloudWatchLogs/commands/copyLogResource.test.ts
index 1405fb7e619..3dc329c8c34 100644
--- a/packages/core/src/test/awsService/cloudWatchLogs/commands/copyLogResource.test.ts
+++ b/packages/core/src/test/awsService/cloudWatchLogs/commands/copyLogResource.test.ts
@@ -5,8 +5,8 @@
import assert from 'assert'
import * as vscode from 'vscode'
-import { createURIFromArgs } from '../../../../awsService/cloudWatchLogs/cloudWatchLogsUtils'
import { copyLogResource } from '../../../../awsService/cloudWatchLogs/commands/copyLogResource'
+import { cwlUriSchema } from '../../../../awsService/cloudWatchLogs/cloudWatchLogsUtils'
describe('copyLogResource', async function () {
beforeEach(async function () {
@@ -23,7 +23,7 @@ describe('copyLogResource', async function () {
regionName: 'region',
streamName: 'stream',
}
- const uri = createURIFromArgs(logGroupWithStream, {})
+ const uri = cwlUriSchema.form({ logGroupInfo: logGroupWithStream, parameters: {} })
await copyLogResource(uri)
@@ -35,7 +35,7 @@ describe('copyLogResource', async function () {
groupName: 'group2',
regionName: 'region2',
}
- const uri = createURIFromArgs(logGroup, {})
+ const uri = cwlUriSchema.form({ logGroupInfo: logGroup, parameters: {} })
await copyLogResource(uri)
diff --git a/packages/core/src/test/awsService/cloudWatchLogs/commands/saveCurrentLogDataContent.test.ts b/packages/core/src/test/awsService/cloudWatchLogs/commands/saveCurrentLogDataContent.test.ts
index 7388b05c718..cd43c91150c 100644
--- a/packages/core/src/test/awsService/cloudWatchLogs/commands/saveCurrentLogDataContent.test.ts
+++ b/packages/core/src/test/awsService/cloudWatchLogs/commands/saveCurrentLogDataContent.test.ts
@@ -7,7 +7,6 @@ import assert from 'assert'
import * as path from 'path'
import * as vscode from 'vscode'
-import { createURIFromArgs } from '../../../../awsService/cloudWatchLogs/cloudWatchLogsUtils'
import { saveCurrentLogDataContent } from '../../../../awsService/cloudWatchLogs/commands/saveCurrentLogDataContent'
import { fileExists, makeTemporaryToolkitFolder, readFileAsString } from '../../../../shared/filesystemUtilities'
import { getTestWindow } from '../../../shared/vscode/window'
@@ -18,6 +17,7 @@ import {
LogDataRegistry,
} from '../../../../awsService/cloudWatchLogs/registry/logDataRegistry'
import { assertTextEditorContains } from '../../../testUtil'
+import { cwlUriSchema } from '../../../../awsService/cloudWatchLogs/cloudWatchLogsUtils'
import { fs } from '../../../../shared'
async function testFilterLogEvents(
@@ -55,7 +55,7 @@ describe('saveCurrentLogDataContent', async function () {
regionName: 'r',
streamName: 's',
}
- const uri = createURIFromArgs(logGroupInfo, {})
+ const uri = cwlUriSchema.form({ logGroupInfo: logGroupInfo, parameters: {} })
LogDataRegistry.instance.registerInitialLog(uri, testFilterLogEvents)
await LogDataRegistry.instance.fetchNextLogEvents(uri)
await vscode.window.showTextDocument(uri)
diff --git a/packages/core/src/test/awsService/cloudWatchLogs/document/logDataDocumentProvider.test.ts b/packages/core/src/test/awsService/cloudWatchLogs/document/logDataDocumentProvider.test.ts
index 703d1f59cb0..221d0a6fee3 100644
--- a/packages/core/src/test/awsService/cloudWatchLogs/document/logDataDocumentProvider.test.ts
+++ b/packages/core/src/test/awsService/cloudWatchLogs/document/logDataDocumentProvider.test.ts
@@ -7,7 +7,7 @@ import assert from 'assert'
import * as vscode from 'vscode'
import {
CloudWatchLogsSettings,
- createURIFromArgs,
+ cwlUriSchema,
isLogStreamUri,
} from '../../../../awsService/cloudWatchLogs/cloudWatchLogsUtils'
import { LogDataDocumentProvider } from '../../../../awsService/cloudWatchLogs/document/logDataDocumentProvider'
@@ -64,7 +64,7 @@ describe('LogDataDocumentProvider', async function () {
regionName: 'region',
streamName: 'stream',
}
- const getLogsUri = createURIFromArgs(getLogsLogGroupInfo, {})
+ const getLogsUri = cwlUriSchema.form({ logGroupInfo: getLogsLogGroupInfo, parameters: {} })
const filterLogsStream: CloudWatchLogsData = {
events: [],
@@ -77,7 +77,10 @@ describe('LogDataDocumentProvider', async function () {
busy: false,
}
- const filterLogsUri = createURIFromArgs(filterLogsStream.logGroupInfo, filterLogsStream.parameters)
+ const filterLogsUri = cwlUriSchema.form({
+ logGroupInfo: filterLogsStream.logGroupInfo,
+ parameters: filterLogsStream.parameters,
+ })
before(function () {
config = new Settings(vscode.ConfigurationTarget.Workspace)
@@ -130,7 +133,7 @@ describe('LogDataDocumentProvider', async function () {
regionName: 'regionA',
streamName: 'streamA',
}
- const logStreamNameUri = createURIFromArgs(logGroupInfo, {})
+ const logStreamNameUri = cwlUriSchema.form({ logGroupInfo: logGroupInfo, parameters: {} })
const events: FilteredLogEvent[] = [
{
diff --git a/packages/core/src/test/awsService/cloudWatchLogs/document/logStreamsCodeLensProvider.test.ts b/packages/core/src/test/awsService/cloudWatchLogs/document/logStreamsCodeLensProvider.test.ts
index 52ab01b30bb..ae222fd25df 100644
--- a/packages/core/src/test/awsService/cloudWatchLogs/document/logStreamsCodeLensProvider.test.ts
+++ b/packages/core/src/test/awsService/cloudWatchLogs/document/logStreamsCodeLensProvider.test.ts
@@ -9,8 +9,8 @@ import { LogDataRegistry } from '../../../../awsService/cloudWatchLogs/registry/
import { LogDataDocumentProvider } from '../../../../awsService/cloudWatchLogs/document/logDataDocumentProvider'
import { CancellationToken, CodeLens, TextDocument } from 'vscode'
import assert = require('assert')
-import { createURIFromArgs } from '../../../../awsService/cloudWatchLogs/cloudWatchLogsUtils'
import { createStubInstance, SinonStubbedInstance } from 'sinon'
+import { cwlUriSchema } from '../../../../awsService/cloudWatchLogs/cloudWatchLogsUtils'
describe('LogStreamCodeLensProvider', async () => {
describe('provideCodeLenses()', async () => {
@@ -18,7 +18,7 @@ describe('LogStreamCodeLensProvider', async () => {
let documentProvider: SinonStubbedInstance
const logGroupInfo = { groupName: 'MyGroupName', regionName: 'MyRegionName' }
- const logUri = createURIFromArgs(logGroupInfo, {})
+ const logUri = cwlUriSchema.form({ logGroupInfo: logGroupInfo, parameters: {} })
before(async () => {
const registry: LogDataRegistry = {} as LogDataRegistry
diff --git a/packages/core/src/test/awsService/cloudWatchLogs/registry/logDataRegistry.test.ts b/packages/core/src/test/awsService/cloudWatchLogs/registry/logDataRegistry.test.ts
index 108b6843b8a..f9b253d941f 100644
--- a/packages/core/src/test/awsService/cloudWatchLogs/registry/logDataRegistry.test.ts
+++ b/packages/core/src/test/awsService/cloudWatchLogs/registry/logDataRegistry.test.ts
@@ -14,7 +14,7 @@ import {
CloudWatchLogsData,
} from '../../../../awsService/cloudWatchLogs/registry/logDataRegistry'
import { Settings } from '../../../../shared/settings'
-import { CloudWatchLogsSettings, createURIFromArgs } from '../../../../awsService/cloudWatchLogs/cloudWatchLogsUtils'
+import { CloudWatchLogsSettings, cwlUriSchema } from '../../../../awsService/cloudWatchLogs/cloudWatchLogsUtils'
import {
backwardToken,
fakeGetLogEvents,
@@ -34,11 +34,23 @@ describe('LogDataRegistry', async function () {
const config = new Settings(vscode.ConfigurationTarget.Workspace)
- const registeredUri = createURIFromArgs(testLogData.logGroupInfo, testLogData.parameters)
- const unregisteredUri = createURIFromArgs(unregisteredData.logGroupInfo, unregisteredData.parameters)
- const newLineUri = createURIFromArgs(newLineData.logGroupInfo, newLineData.parameters)
- const searchLogGroupUri = createURIFromArgs(logGroupsData.logGroupInfo, logGroupsData.parameters)
- const paginatedUri = createURIFromArgs(paginatedData.logGroupInfo, paginatedData.parameters)
+ const registeredUri = cwlUriSchema.form({
+ logGroupInfo: testLogData.logGroupInfo,
+ parameters: testLogData.parameters,
+ })
+ const unregisteredUri = cwlUriSchema.form({
+ logGroupInfo: unregisteredData.logGroupInfo,
+ parameters: unregisteredData.parameters,
+ })
+ const newLineUri = cwlUriSchema.form({ logGroupInfo: newLineData.logGroupInfo, parameters: newLineData.parameters })
+ const searchLogGroupUri = cwlUriSchema.form({
+ logGroupInfo: logGroupsData.logGroupInfo,
+ parameters: logGroupsData.parameters,
+ })
+ const paginatedUri = cwlUriSchema.form({
+ logGroupInfo: paginatedData.logGroupInfo,
+ parameters: paginatedData.parameters,
+ })
/**
* Only intended to expose the {get|set}LogData methods for testing purposes.
diff --git a/packages/core/src/test/awsService/cloudWatchLogs/utils.test.ts b/packages/core/src/test/awsService/cloudWatchLogs/utils.test.ts
index eed2b709c40..02a768c4f07 100644
--- a/packages/core/src/test/awsService/cloudWatchLogs/utils.test.ts
+++ b/packages/core/src/test/awsService/cloudWatchLogs/utils.test.ts
@@ -6,11 +6,7 @@
import assert from 'assert'
import { CloudWatchLogs } from 'aws-sdk'
import * as vscode from 'vscode'
-import {
- createURIFromArgs,
- parseCloudWatchLogsUri,
- uriToKey,
-} from '../../../awsService/cloudWatchLogs/cloudWatchLogsUtils'
+import { cwlUriSchema, uriToKey } from '../../../awsService/cloudWatchLogs/cloudWatchLogsUtils'
import {
CloudWatchLogsParameters,
CloudWatchLogsData,
@@ -170,28 +166,31 @@ export const paginatedData: CloudWatchLogsData = {
retrieveLogsFunction: returnPaginatedEvents,
busy: false,
}
-export const goodUri = createURIFromArgs(testComponents.logGroupInfo, testComponents.parameters)
+export const goodUri = cwlUriSchema.form({
+ logGroupInfo: testComponents.logGroupInfo,
+ parameters: testComponents.parameters,
+})
describe('parseCloudWatchLogsUri', async function () {
it('converts a valid URI to components', function () {
- const result = parseCloudWatchLogsUri(goodUri)
+ const result = cwlUriSchema.parse(goodUri)
assert.deepStrictEqual(result.logGroupInfo, testComponents.logGroupInfo)
assert.deepStrictEqual(result.parameters, testComponents.parameters)
})
it('does not convert URIs with an invalid scheme', async function () {
assert.throws(() => {
- parseCloudWatchLogsUri(vscode.Uri.parse('wrong:scheme'))
+ cwlUriSchema.parse(vscode.Uri.parse('wrong:scheme'))
})
})
it('does not convert URIs with more or less than three elements', async function () {
assert.throws(() => {
- parseCloudWatchLogsUri(vscode.Uri.parse(`${CLOUDWATCH_LOGS_SCHEME}:elementOne:elementTwo`))
+ cwlUriSchema.parse(vscode.Uri.parse(`${CLOUDWATCH_LOGS_SCHEME}:elementOne:elementTwo`))
})
assert.throws(() => {
- parseCloudWatchLogsUri(
+ cwlUriSchema.parse(
vscode.Uri.parse(`${CLOUDWATCH_LOGS_SCHEME}:elementOne:elementTwo:elementThree:whoopsAllElements`)
)
})
@@ -208,7 +207,7 @@ describe('createURIFromArgs', function () {
)}`
)
assert.deepStrictEqual(testUri, goodUri)
- const newTestComponents = parseCloudWatchLogsUri(testUri)
+ const newTestComponents = cwlUriSchema.parse(testUri)
assert.deepStrictEqual(testComponents, newTestComponents)
})
})
@@ -230,8 +229,8 @@ describe('uriToKey', function () {
it('creates the same key for different order query', function () {
const param1: CloudWatchLogsParameters = { filterPattern: 'same', startTime: 0 }
const param2: CloudWatchLogsParameters = { startTime: 0, filterPattern: 'same' }
- const firstOrder = createURIFromArgs(testComponents.logGroupInfo, param1)
- const secondOrder = createURIFromArgs(testComponents.logGroupInfo, param2)
+ const firstOrder = cwlUriSchema.form({ logGroupInfo: testComponents.logGroupInfo, parameters: param1 })
+ const secondOrder = cwlUriSchema.form({ logGroupInfo: testComponents.logGroupInfo, parameters: param2 })
assert.notDeepStrictEqual(firstOrder, secondOrder)
assert.strictEqual(uriToKey(firstOrder), uriToKey(secondOrder))
diff --git a/packages/core/src/test/awsService/ec2/ec2LogDocumentProvider.test.ts b/packages/core/src/test/awsService/ec2/ec2LogDocumentProvider.test.ts
new file mode 100644
index 00000000000..3889be0ac9f
--- /dev/null
+++ b/packages/core/src/test/awsService/ec2/ec2LogDocumentProvider.test.ts
@@ -0,0 +1,39 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'assert'
+import * as vscode from 'vscode'
+import * as sinon from 'sinon'
+import { Ec2LogDocumentProvider } from '../../../awsService/ec2/ec2LogDocumentProvider'
+import { ec2LogsScheme } from '../../../shared/constants'
+import { Ec2Client } from '../../../shared/clients/ec2Client'
+
+describe('LogDataDocumentProvider', async function () {
+ let provider: Ec2LogDocumentProvider
+
+ before(function () {
+ provider = new Ec2LogDocumentProvider()
+ })
+
+ it('throws error on attempt to get content from other schemes', async function () {
+ const wrongSchemeUri = vscode.Uri.parse(`ec2-not:us-west1:id`)
+
+ await assert.rejects(async () => await provider.provideTextDocumentContent(wrongSchemeUri), {
+ message: `Invalid EC2 Logs URI: ${wrongSchemeUri.toString()}`,
+ })
+ })
+
+ it('fetches content for valid ec2 log URI', async function () {
+ const validUri = vscode.Uri.parse(`${ec2LogsScheme}:us-west1:instance1`)
+ const expectedContent = 'log content'
+ sinon.stub(Ec2Client.prototype, 'getConsoleOutput').resolves({
+ InstanceId: 'instance1',
+ Output: expectedContent,
+ })
+ const content = await provider.provideTextDocumentContent(validUri)
+ assert.strictEqual(content, expectedContent)
+ sinon.restore()
+ })
+})
diff --git a/packages/core/src/test/setupUtil.ts b/packages/core/src/test/setupUtil.ts
index 6a46514cf06..f158d67ad55 100644
--- a/packages/core/src/test/setupUtil.ts
+++ b/packages/core/src/test/setupUtil.ts
@@ -11,6 +11,7 @@ import { hasKey } from '../shared/utilities/tsUtils'
import { getTestWindow, printPendingUiElements } from './shared/vscode/window'
import { ToolkitError, formatError } from '../shared/errors'
import { proceedToBrowser } from '../auth/sso/model'
+import { decodeBase64 } from '../shared'
const runnableTimeout = Symbol('runnableTimeout')
@@ -164,7 +165,7 @@ export async function invokeLambda(id: string, request: unknown): Promise
Date: Fri, 18 Oct 2024 16:44:32 -0400
Subject: [PATCH 69/87] test(ec2): ensure that private key is not written to
telemetry (#5779)
## Problem
We want to enforce that the private key generated by the toolkit is not
accidentally slipped into a telemetry metric.
## Solution
Similar to `assertTelemetry`, we implement a `assertNoTelemetryMatch`
test utility that scans all metrics to see if a specified keyword is
included in its keys or values.
With this utility, it is relatively straightforward to assert that this
private key doesn't appear in the telemetry when we generate it and
perform other operations with it.
---
License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---------
Co-authored-by: Justin M. Keyes
---
.../core/src/awsService/ec2/sshKeyPair.ts | 27 ++++++++++++++-----
.../src/shared/telemetry/telemetryLogger.ts | 7 +++++
.../src/test/awsService/ec2/model.test.ts | 22 +++++++++++++++
.../test/awsService/ec2/sshKeyPair.test.ts | 13 +++++----
packages/core/src/test/testUtil.ts | 7 +++++
5 files changed, 62 insertions(+), 14 deletions(-)
diff --git a/packages/core/src/awsService/ec2/sshKeyPair.ts b/packages/core/src/awsService/ec2/sshKeyPair.ts
index 0e7ab3ea3c3..b09b4824a52 100644
--- a/packages/core/src/awsService/ec2/sshKeyPair.ts
+++ b/packages/core/src/awsService/ec2/sshKeyPair.ts
@@ -2,7 +2,8 @@
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
-import { fs } from '../../shared'
+import os from 'os'
+import { fs, globals } from '../../shared'
import { ToolkitError } from '../../shared/errors'
import { tryRun } from '../../shared/utilities/pathFind'
import { Timeout } from '../../shared/utilities/timeoutUtils'
@@ -32,12 +33,24 @@ export class SshKeyPair {
return new SshKeyPair(keyPath, lifetime)
}
- public static async generateSshKeyPair(keyPath: string): Promise {
- const keyGenerated = await this.tryKeyTypes(keyPath, ['ed25519', 'rsa'])
+ private static async assertGenerated(keyPath: string, keyGenerated: boolean): Promise {
if (!keyGenerated) {
- throw new ToolkitError('ec2: Unable to generate ssh key pair')
+ throw new ToolkitError('ec2: Unable to generate ssh key pair with either ed25519 or rsa')
+ }
+
+ if (!(await fs.exists(keyPath))) {
+ throw new ToolkitError(`ec2: Failed to generate keys, resulting key not found at ${keyPath}`)
+ }
+ }
+
+ public static async generateSshKeyPair(keyPath: string): Promise {
+ const keyGenerated = await SshKeyPair.tryKeyTypes(keyPath, ['ed25519', 'rsa'])
+ await SshKeyPair.assertGenerated(keyPath, keyGenerated)
+ // Should already be the case, but just in case we assert permissions.
+ // skip on Windows since it only allows write permission to be changed.
+ if (!globals.isWeb && os.platform() !== 'win32') {
+ await fs.chmod(keyPath, 0o600)
}
- await fs.chmod(keyPath, 0o600)
}
/**
* Attempts to generate an ssh key pair. Returns true if successful, false otherwise.
@@ -72,8 +85,8 @@ export class SshKeyPair {
}
public async delete(): Promise {
- await fs.delete(this.keyPath)
- await fs.delete(this.publicKeyPath)
+ await fs.delete(this.keyPath, { force: true })
+ await fs.delete(this.publicKeyPath, { force: true })
if (!this.lifeTimeout.completed) {
this.lifeTimeout.cancel()
diff --git a/packages/core/src/shared/telemetry/telemetryLogger.ts b/packages/core/src/shared/telemetry/telemetryLogger.ts
index e5b03d7b444..c01905f6260 100644
--- a/packages/core/src/shared/telemetry/telemetryLogger.ts
+++ b/packages/core/src/shared/telemetry/telemetryLogger.ts
@@ -118,4 +118,11 @@ export class TelemetryLogger {
public queryFull(query: MetricQuery): MetricDatum[] {
return this._metrics.filter((m) => m.MetricName === query.metricName)
}
+
+ /**
+ * Queries telemetry for metrics with metadata key or value matching the given regex.
+ */
+ public queryRegex(re: RegExp | string): MetricDatum[] {
+ return this._metrics.filter((m) => m.Metadata?.some((md) => md.Value?.match(re) || md.Key?.match(re)))
+ }
}
diff --git a/packages/core/src/test/awsService/ec2/model.test.ts b/packages/core/src/test/awsService/ec2/model.test.ts
index 36a7895a35f..6e750f6ebfe 100644
--- a/packages/core/src/test/awsService/ec2/model.test.ts
+++ b/packages/core/src/test/awsService/ec2/model.test.ts
@@ -13,6 +13,9 @@ import { ToolkitError } from '../../../shared/errors'
import { IAM } from 'aws-sdk'
import { SshKeyPair } from '../../../awsService/ec2/sshKeyPair'
import { DefaultIamClient } from '../../../shared/clients/iamClient'
+import { assertNoTelemetryMatch, createTestWorkspaceFolder } from '../../testUtil'
+import { fs } from '../../../shared'
+import path from 'path'
describe('Ec2ConnectClient', function () {
let client: Ec2ConnectionManager
@@ -144,6 +147,25 @@ describe('Ec2ConnectClient', function () {
sinon.assert.calledWith(sendCommandStub, testSelection.instanceId, 'AWS-RunShellScript')
sinon.restore()
})
+
+ it('avoids writing the keys to any telemetry metrics', async function () {
+ sinon.stub(SsmClient.prototype, 'sendCommandAndWait')
+
+ const testSelection = {
+ instanceId: 'test-id',
+ region: 'test-region',
+ }
+ const testWorkspaceFolder = await createTestWorkspaceFolder()
+ const keyPath = path.join(testWorkspaceFolder.uri.fsPath, 'key')
+ const keys = await SshKeyPair.getSshKeyPair(keyPath, 60000)
+ await client.sendSshKeyToInstance(testSelection, keys, 'test-user')
+ const privKey = await fs.readFileText(keyPath)
+ assertNoTelemetryMatch(privKey)
+ sinon.restore()
+
+ await keys.delete()
+ await fs.delete(testWorkspaceFolder.uri, { force: true })
+ })
})
describe('getRemoteUser', async function () {
diff --git a/packages/core/src/test/awsService/ec2/sshKeyPair.test.ts b/packages/core/src/test/awsService/ec2/sshKeyPair.test.ts
index 92b1f123189..25b0578694c 100644
--- a/packages/core/src/test/awsService/ec2/sshKeyPair.test.ts
+++ b/packages/core/src/test/awsService/ec2/sshKeyPair.test.ts
@@ -2,7 +2,6 @@
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
-import * as vscode from 'vscode'
import assert from 'assert'
import nodefs from 'fs' // eslint-disable-line no-restricted-imports
import * as sinon from 'sinon'
@@ -14,7 +13,7 @@ import { InstalledClock } from '@sinonjs/fake-timers'
import { ChildProcess } from '../../../shared/utilities/processUtils'
import { fs, globals } from '../../../shared'
-describe('SshKeyUtility', async function () {
+describe('SshKeyPair', async function () {
let temporaryDirectory: string
let keyPath: string
let keyPair: SshKeyPair
@@ -41,14 +40,14 @@ describe('SshKeyUtility', async function () {
})
it('generates key in target file', async function () {
- const contents = await fs.readFileBytes(vscode.Uri.file(keyPath))
+ const contents = await fs.readFileBytes(keyPath)
assert.notStrictEqual(contents.length, 0)
})
it('generates unique key each time', async function () {
- const beforeContent = await fs.readFileBytes(vscode.Uri.file(keyPath))
+ const beforeContent = await fs.readFileBytes(keyPath)
keyPair = await SshKeyPair.getSshKeyPair(keyPath, 30000)
- const afterContent = await fs.readFileBytes(vscode.Uri.file(keyPath))
+ const afterContent = await fs.readFileBytes(keyPath)
assert.notStrictEqual(beforeContent, afterContent)
})
@@ -90,10 +89,10 @@ describe('SshKeyUtility', async function () {
it('does overwrite existing keys on get call', async function () {
const generateStub = sinon.spy(SshKeyPair, 'generateSshKeyPair')
- const keyBefore = await fs.readFileBytes(vscode.Uri.file(keyPath))
+ const keyBefore = await fs.readFileBytes(keyPath)
keyPair = await SshKeyPair.getSshKeyPair(keyPath, 30000)
- const keyAfter = await fs.readFileBytes(vscode.Uri.file(keyPath))
+ const keyAfter = await fs.readFileBytes(keyPath)
sinon.assert.calledOnce(generateStub)
assert.notStrictEqual(keyBefore, keyAfter)
diff --git a/packages/core/src/test/testUtil.ts b/packages/core/src/test/testUtil.ts
index 26b0c473403..dbe7c6e6437 100644
--- a/packages/core/src/test/testUtil.ts
+++ b/packages/core/src/test/testUtil.ts
@@ -295,6 +295,13 @@ export function partialDeepCompare(actual: unknown, expected: T, message?: st
const partial = selectFrom(actual, ...keys(expected as object))
assert.deepStrictEqual(partial, expected, message)
}
+/**
+ * Asserts that no metrics metadata (key OR value) matches the given regex.
+ * @param keyword target substring to search for
+ */
+export function assertNoTelemetryMatch(re: RegExp | string): void | never {
+ return assert.ok(globals.telemetry.logger.queryRegex(re).length === 0)
+}
/**
* Finds the emitted telemetry metrics with the given `name`, then checks if the metadata fields
From 7528a6200dface15448d491741d229a44dc15111 Mon Sep 17 00:00:00 2001
From: Nikolas Komonen <118216176+nkomonen-amazon@users.noreply.github.com>
Date: Fri, 18 Oct 2024 23:31:14 -0400
Subject: [PATCH 70/87] feat(time): Add Interval class (#5792)
Creates our own version of an Interval, similar to how we have our own
Timeout
---
License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
Signed-off-by: nkomonen-amazon
---
.../core/src/shared/utilities/timeoutUtils.ts | 28 ++++++++++
.../shared/utilities/timeoutUtils.test.ts | 54 +++++++++++++++++++
2 files changed, 82 insertions(+)
diff --git a/packages/core/src/shared/utilities/timeoutUtils.ts b/packages/core/src/shared/utilities/timeoutUtils.ts
index 18922067946..11710589ec0 100644
--- a/packages/core/src/shared/utilities/timeoutUtils.ts
+++ b/packages/core/src/shared/utilities/timeoutUtils.ts
@@ -184,6 +184,34 @@ export class Timeout {
}
}
+export class Interval {
+ private _setCompleted: (() => void) | undefined
+ private _nextCompletion: Promise
+ private ref: NodeJS.Timer | number | undefined
+
+ constructor(intervalMillis: number, onCompletion: () => Promise) {
+ this._nextCompletion = new Promise((resolve) => {
+ this._setCompleted = () => resolve()
+ })
+ this.ref = globals.clock.setInterval(async () => {
+ await onCompletion()
+ this._setCompleted!()
+ this._nextCompletion = new Promise((resolve) => {
+ this._setCompleted = () => resolve()
+ })
+ }, intervalMillis)
+ }
+
+ /** Allows to wait for the next interval to finish running */
+ public async nextCompletion() {
+ await this._nextCompletion
+ }
+
+ public dispose() {
+ globals.clock.clearInterval(this.ref)
+ }
+}
+
interface WaitUntilOptions {
/** Timeout in ms (default: 5000) */
readonly timeout?: number
diff --git a/packages/core/src/test/shared/utilities/timeoutUtils.test.ts b/packages/core/src/test/shared/utilities/timeoutUtils.test.ts
index 473c476b4d2..c518b1cfae1 100644
--- a/packages/core/src/test/shared/utilities/timeoutUtils.test.ts
+++ b/packages/core/src/test/shared/utilities/timeoutUtils.test.ts
@@ -8,15 +8,21 @@ import * as FakeTimers from '@sinonjs/fake-timers'
import * as timeoutUtils from '../../../shared/utilities/timeoutUtils'
import { installFakeClock, tickPromise } from '../../../test/testUtil'
import { sleep } from '../../../shared/utilities/timeoutUtils'
+import { SinonStub, SinonSandbox, createSandbox } from 'sinon'
// We export this describe() so it can be used in the web tests as well
export const timeoutUtilsDescribe = describe('timeoutUtils', async function () {
let clock: FakeTimers.InstalledClock
+ let sandbox: SinonSandbox
before(function () {
clock = installFakeClock()
})
+ beforeEach(function () {
+ sandbox = createSandbox()
+ })
+
after(function () {
clock.uninstall()
})
@@ -24,6 +30,7 @@ export const timeoutUtilsDescribe = describe('timeoutUtils', async function () {
afterEach(function () {
clock.reset()
this.timer?.dispose()
+ sandbox.restore()
})
describe('Timeout', async function () {
@@ -192,6 +199,53 @@ export const timeoutUtilsDescribe = describe('timeoutUtils', async function () {
})
})
+ describe('Interval', async function () {
+ let interval: timeoutUtils.Interval
+ let onCompletionStub: SinonStub
+
+ beforeEach(async function () {
+ onCompletionStub = sandbox.stub()
+ interval = new timeoutUtils.Interval(1000, onCompletionStub)
+ })
+
+ afterEach(async function () {
+ interval?.dispose()
+ })
+
+ it('Executes the callback on an interval', async function () {
+ await clock.tickAsync(999)
+ assert.strictEqual(onCompletionStub.callCount, 0)
+ await clock.tickAsync(1)
+ assert.strictEqual(onCompletionStub.callCount, 1)
+
+ await clock.tickAsync(500)
+ assert.strictEqual(onCompletionStub.callCount, 1)
+ await clock.tickAsync(500)
+ assert.strictEqual(onCompletionStub.callCount, 2)
+
+ await clock.tickAsync(1000)
+ assert.strictEqual(onCompletionStub.callCount, 3)
+ })
+
+ it('allows to wait for next completion', async function () {
+ clock.uninstall()
+
+ let curr = 'Did Not Change'
+
+ const realInterval = new timeoutUtils.Interval(50, async () => {
+ await sleep(50)
+ curr = 'Did Change'
+ })
+
+ const withoutWait = curr
+ await realInterval.nextCompletion()
+ const withWait = curr
+
+ assert.strictEqual(withoutWait, 'Did Not Change')
+ assert.strictEqual(withWait, 'Did Change')
+ })
+ })
+
describe('waitUntil', async function () {
const testSettings = {
callCounter: 0,
From fe6ea9afd0fc5d675b518ac5720fe67b6cfb720e Mon Sep 17 00:00:00 2001
From: Nikolas Komonen <118216176+nkomonen-amazon@users.noreply.github.com>
Date: Fri, 18 Oct 2024 23:31:51 -0400
Subject: [PATCH 71/87] fix(test): flaky test (#5809)
increment the time that we wait for the interval to finish since it runs
slower in ci
---
License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
Signed-off-by: nkomonen-amazon
---
packages/core/src/test/shared/crashMonitoring.test.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/core/src/test/shared/crashMonitoring.test.ts b/packages/core/src/test/shared/crashMonitoring.test.ts
index e231a37a400..6d1b311aa62 100644
--- a/packages/core/src/test/shared/crashMonitoring.test.ts
+++ b/packages/core/src/test/shared/crashMonitoring.test.ts
@@ -186,7 +186,7 @@ export const crashMonitoringTest = async () => {
}
// Give some extra time since there is a lot of file i/o
- await awaitIntervals(oneInterval * 2)
+ await awaitIntervals(oneInterval * 3)
assertCrashedExtensions(latestCrashedExts)
})
From c733e2604cd09589e3e0b3e692d8229033f2a6da Mon Sep 17 00:00:00 2001
From: Maxim Hayes <149123719+hayemaxi@users.noreply.github.com>
Date: Mon, 21 Oct 2024 11:42:28 -0400
Subject: [PATCH 72/87] ci: don't output code coverage table in logs (#5813)
Problem: c8 was not given a reporter, therefore it defaulted to `text`
which is the terminal.
Solution: Set it to `lcov`, which is what we have been using previously.
Note: More work may be needed on the .c8rc.json files. We might not need
the one for core/ anymore. They may not be configured in the optimal
way. Is it behaving how we want it to?
---
License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.
---
packages/toolkit/.c8rc.json | 6 ++++++
1 file changed, 6 insertions(+)
create mode 100644 packages/toolkit/.c8rc.json
diff --git a/packages/toolkit/.c8rc.json b/packages/toolkit/.c8rc.json
new file mode 100644
index 00000000000..347e7c3380b
--- /dev/null
+++ b/packages/toolkit/.c8rc.json
@@ -0,0 +1,6 @@
+{
+ "report-dir": "../../coverage/toolkit",
+ "reporter": ["lcov"],
+ "all": true,
+ "exclude": ["**/test*/**", "**/node_modules/**"]
+}
From d70bb9e33bd006ff6c84f441d09b1107d3fc742c Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Fri, 6 Sep 2024 16:20:56 -0400
Subject: [PATCH 73/87] feat(amazonqFeatureDev): include stop generation based
on token cancellation
---
.../core/src/amazonqFeatureDev/session/session.ts | 1 +
.../src/amazonqFeatureDev/session/sessionState.ts | 14 ++++++++++----
packages/core/src/amazonqFeatureDev/types.ts | 2 +-
3 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/packages/core/src/amazonqFeatureDev/session/session.ts b/packages/core/src/amazonqFeatureDev/session/session.ts
index 685f2605971..0f08715e48d 100644
--- a/packages/core/src/amazonqFeatureDev/session/session.ts
+++ b/packages/core/src/amazonqFeatureDev/session/session.ts
@@ -132,6 +132,7 @@ export class Session {
telemetry: this.telemetry,
tokenSource: this.state.tokenSource,
uploadHistory: this.state.uploadHistory,
+ tokenSource: this.state.tokenSource,
})
if (resp.nextState) {
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index 36b659629f7..4833fc1aa1a 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -300,6 +300,12 @@ export class CodeGenState extends CodeGenBase implements SessionState {
credentialStartUrl: AuthUtil.instance.startUrl,
})
+ action.tokenSource?.token.onCancellationRequested(() => {
+ if (action.tokenSource) {
+ this.tokenSource = action.tokenSource
+ }
+ })
+
action.telemetry.setGenerateCodeIteration(this.currentIteration)
action.telemetry.setGenerateCodeLastInvocationTime()
const codeGenerationId = randomUUID()
@@ -363,10 +369,10 @@ export class CodeGenState extends CodeGenBase implements SessionState {
this.currentIteration + 1,
this.codeGenerationRemainingIterationCount,
this.codeGenerationTotalIterationCount,
- this.tokenSource,
this.currentCodeGenerationId,
action.uploadHistory,
- codeGenerationId
+ codeGenerationId,
+ this.tokenSource
)
return {
nextState,
@@ -489,10 +495,10 @@ export class PrepareCodeGenState implements SessionState {
public codeGenerationRemainingIterationCount?: number,
public codeGenerationTotalIterationCount?: number,
- public superTokenSource?: vscode.CancellationTokenSource,
public currentCodeGenerationId?: string,
public uploadHistory: UploadHistory = {},
- public codeGenerationId?: string
+ public codeGenerationId?: string,
+ public superTokenSource?: vscode.CancellationTokenSource
) {
this.tokenSource = superTokenSource || new vscode.CancellationTokenSource()
this.uploadId = config.uploadId
diff --git a/packages/core/src/amazonqFeatureDev/types.ts b/packages/core/src/amazonqFeatureDev/types.ts
index c4267d91e29..cf046743425 100644
--- a/packages/core/src/amazonqFeatureDev/types.ts
+++ b/packages/core/src/amazonqFeatureDev/types.ts
@@ -88,8 +88,8 @@ export interface SessionStateAction {
messenger: Messenger
fs: VirtualFileSystem
telemetry: TelemetryHelper
- tokenSource?: CancellationTokenSource
uploadHistory?: UploadHistory
+ tokenSource?: CancellationTokenSource
}
export type NewFileZipContents = { zipFilePath: string; fileContent: string }
From b99f4db1ed84b3fd122e56857b44d88854222ac1 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Wed, 11 Sep 2024 14:49:11 -0400
Subject: [PATCH 74/87] refactor(amazonqFeatureDev): include updated rts model
---
packages/core/src/amazonqFeatureDev/client/featureDev.ts | 3 +++
packages/core/src/amazonqFeatureDev/session/session.ts | 5 +++++
packages/core/src/amazonqFeatureDev/session/sessionState.ts | 5 ++++-
3 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/packages/core/src/amazonqFeatureDev/client/featureDev.ts b/packages/core/src/amazonqFeatureDev/client/featureDev.ts
index 8cd5ee97f90..2038de1ece4 100644
--- a/packages/core/src/amazonqFeatureDev/client/featureDev.ts
+++ b/packages/core/src/amazonqFeatureDev/client/featureDev.ts
@@ -134,7 +134,10 @@ export class FeatureDevClient {
conversationId: string,
uploadId: string,
message: string,
+<<<<<<< HEAD
codeGenerationId: string,
+=======
+>>>>>>> 4742b3c59 (refactor(amazonqFeatureDev): include updated rts model)
currentCodeGenerationId?: string
) {
try {
diff --git a/packages/core/src/amazonqFeatureDev/session/session.ts b/packages/core/src/amazonqFeatureDev/session/session.ts
index 0f08715e48d..3384ca0ad8f 100644
--- a/packages/core/src/amazonqFeatureDev/session/session.ts
+++ b/packages/core/src/amazonqFeatureDev/session/session.ts
@@ -109,6 +109,7 @@ export class Session {
workspaceFolders: this.config.workspaceFolders,
proxyClient: this.proxyClient,
conversationId: this.conversationId,
+ currentCodeGenerationId: this.currentCodeGenerationId as string,
}
}
@@ -137,9 +138,13 @@ export class Session {
if (resp.nextState) {
// Cancel the request before moving to a new state
+<<<<<<< HEAD
if (!this.state?.tokenSource?.token.isCancellationRequested) {
this.state?.tokenSource?.cancel()
}
+=======
+ this.state?.tokenSource?.cancel()
+>>>>>>> 4742b3c59 (refactor(amazonqFeatureDev): include updated rts model)
// Move to the next state
this._state = resp.nextState
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index 4833fc1aa1a..5336f522490 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -147,6 +147,7 @@ abstract class CodeGenBase {
public tabID: string
) {
this.tokenSource = new vscode.CancellationTokenSource()
+ this.isCancellationRequested = false
this.conversationId = config.conversationId
this.uploadId = config.uploadId
this.currentCodeGenerationId = config.currentCodeGenerationId || EmptyCodeGenID
@@ -304,6 +305,9 @@ export class CodeGenState extends CodeGenBase implements SessionState {
if (action.tokenSource) {
this.tokenSource = action.tokenSource
}
+ this.isCancellationRequested = true
+ action.tokenSource?.dispose()
+ action.tokenSource = undefined
})
action.telemetry.setGenerateCodeIteration(this.currentIteration)
@@ -492,7 +496,6 @@ export class PrepareCodeGenState implements SessionState {
public references: CodeReference[],
public tabID: string,
public currentIteration: number,
-
public codeGenerationRemainingIterationCount?: number,
public codeGenerationTotalIterationCount?: number,
public currentCodeGenerationId?: string,
From 5bfde04e36c4a3d8adf115d7614ba0d39347cffe Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Thu, 12 Sep 2024 10:52:20 -0400
Subject: [PATCH 75/87] fix(amazonqFeatureDev): include currentCodeGenerationId
to track reference for stop code generation
---
.../core/src/amazonqFeatureDev/session/session.ts | 1 -
.../src/amazonqFeatureDev/session/sessionState.ts | 14 ++++++++++----
2 files changed, 10 insertions(+), 5 deletions(-)
diff --git a/packages/core/src/amazonqFeatureDev/session/session.ts b/packages/core/src/amazonqFeatureDev/session/session.ts
index 3384ca0ad8f..56c3edec064 100644
--- a/packages/core/src/amazonqFeatureDev/session/session.ts
+++ b/packages/core/src/amazonqFeatureDev/session/session.ts
@@ -109,7 +109,6 @@ export class Session {
workspaceFolders: this.config.workspaceFolders,
proxyClient: this.proxyClient,
conversationId: this.conversationId,
- currentCodeGenerationId: this.currentCodeGenerationId as string,
}
}
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index 5336f522490..997add72ad3 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -306,6 +306,9 @@ export class CodeGenState extends CodeGenBase implements SessionState {
this.tokenSource = action.tokenSource
}
this.isCancellationRequested = true
+ if (action.tokenSource) {
+ this.tokenSource = action.tokenSource
+ }
action.tokenSource?.dispose()
action.tokenSource = undefined
})
@@ -321,6 +324,9 @@ export class CodeGenState extends CodeGenBase implements SessionState {
this.currentCodeGenerationId
)
+ this.currentCodeGenerationId = codeGenerationId
+ this.config.currentCodeGenerationId = codeGenerationId
+
if (!this.isCancellationRequested) {
action.messenger.sendAnswer({
message: i18n('AWS.amazonq.featureDev.pillText.generatingCode'),
@@ -373,10 +379,10 @@ export class CodeGenState extends CodeGenBase implements SessionState {
this.currentIteration + 1,
this.codeGenerationRemainingIterationCount,
this.codeGenerationTotalIterationCount,
- this.currentCodeGenerationId,
action.uploadHistory,
codeGenerationId,
- this.tokenSource
+ this.tokenSource,
+ this.currentCodeGenerationId
)
return {
nextState,
@@ -498,10 +504,10 @@ export class PrepareCodeGenState implements SessionState {
public currentIteration: number,
public codeGenerationRemainingIterationCount?: number,
public codeGenerationTotalIterationCount?: number,
- public currentCodeGenerationId?: string,
public uploadHistory: UploadHistory = {},
public codeGenerationId?: string,
- public superTokenSource?: vscode.CancellationTokenSource
+ public superTokenSource?: vscode.CancellationTokenSource,
+ public currentCodeGenerationId?: string
) {
this.tokenSource = superTokenSource || new vscode.CancellationTokenSource()
this.uploadId = config.uploadId
From 14a620aab1fecd877db65176048b5214d937816c Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Tue, 17 Sep 2024 13:12:01 -0400
Subject: [PATCH 76/87] refactor(amazonqFeatureDev): add logic for
codeGenerationId
---
packages/core/src/amazonqFeatureDev/client/featureDev.ts | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/packages/core/src/amazonqFeatureDev/client/featureDev.ts b/packages/core/src/amazonqFeatureDev/client/featureDev.ts
index 2038de1ece4..374e8b17949 100644
--- a/packages/core/src/amazonqFeatureDev/client/featureDev.ts
+++ b/packages/core/src/amazonqFeatureDev/client/featureDev.ts
@@ -134,10 +134,14 @@ export class FeatureDevClient {
conversationId: string,
uploadId: string,
message: string,
+<<<<<<< HEAD
<<<<<<< HEAD
codeGenerationId: string,
=======
>>>>>>> 4742b3c59 (refactor(amazonqFeatureDev): include updated rts model)
+=======
+ codeGenerationId: string,
+>>>>>>> d9c61d527 (refactor(amazonqFeatureDev): add logic for codeGenerationId)
currentCodeGenerationId?: string
) {
try {
From 12e137622607927310139f4e1f2d7a7144a207a4 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Wed, 25 Sep 2024 09:53:40 -0400
Subject: [PATCH 77/87] fix(amazonqFeatureDev): create a new token to stop and
iterate in the same session
---
.../core/src/amazonqFeatureDev/session/session.ts | 4 ++++
.../src/amazonqFeatureDev/session/sessionState.ts | 12 +++++-------
2 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/packages/core/src/amazonqFeatureDev/session/session.ts b/packages/core/src/amazonqFeatureDev/session/session.ts
index 56c3edec064..4db9f7c557c 100644
--- a/packages/core/src/amazonqFeatureDev/session/session.ts
+++ b/packages/core/src/amazonqFeatureDev/session/session.ts
@@ -137,6 +137,7 @@ export class Session {
if (resp.nextState) {
// Cancel the request before moving to a new state
+<<<<<<< HEAD
<<<<<<< HEAD
if (!this.state?.tokenSource?.token.isCancellationRequested) {
this.state?.tokenSource?.cancel()
@@ -144,6 +145,9 @@ export class Session {
=======
this.state?.tokenSource?.cancel()
>>>>>>> 4742b3c59 (refactor(amazonqFeatureDev): include updated rts model)
+=======
+ if (!this.state?.tokenSource?.token.isCancellationRequested) this.state?.tokenSource?.cancel()
+>>>>>>> e6782b66f (fix(amazonqFeatureDev): create a new token to stop and iterate in the same session)
// Move to the next state
this._state = resp.nextState
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index 997add72ad3..33e71ee46c7 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -306,9 +306,6 @@ export class CodeGenState extends CodeGenBase implements SessionState {
this.tokenSource = action.tokenSource
}
this.isCancellationRequested = true
- if (action.tokenSource) {
- this.tokenSource = action.tokenSource
- }
action.tokenSource?.dispose()
action.tokenSource = undefined
})
@@ -324,9 +321,6 @@ export class CodeGenState extends CodeGenBase implements SessionState {
this.currentCodeGenerationId
)
- this.currentCodeGenerationId = codeGenerationId
- this.config.currentCodeGenerationId = codeGenerationId
-
if (!this.isCancellationRequested) {
action.messenger.sendAnswer({
message: i18n('AWS.amazonq.featureDev.pillText.generatingCode'),
@@ -541,7 +535,11 @@ export class PrepareCodeGenState implements SessionState {
span
)
const uploadId = randomUUID()
- const { uploadUrl, kmsKeyArn } = await this.config.proxyClient.createUploadUrl(
+ const {
+ uploadUrl,
+ uploadId: returnedUploadId,
+ kmsKeyArn,
+ } = await this.config.proxyClient.createUploadUrl(
this.config.conversationId,
zipFileChecksum,
zipFileBuffer.length,
From 7b7da926b3352d59079b234f82db1da90f969372 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Thu, 26 Sep 2024 13:47:42 -0400
Subject: [PATCH 78/87] fix(dev): apply fixes based on new eslint rules
---
.../core/src/amazonqFeatureDev/session/session.ts | 11 -----------
.../src/amazonqFeatureDev/session/sessionState.ts | 6 +-----
2 files changed, 1 insertion(+), 16 deletions(-)
diff --git a/packages/core/src/amazonqFeatureDev/session/session.ts b/packages/core/src/amazonqFeatureDev/session/session.ts
index 4db9f7c557c..204e974eee0 100644
--- a/packages/core/src/amazonqFeatureDev/session/session.ts
+++ b/packages/core/src/amazonqFeatureDev/session/session.ts
@@ -132,23 +132,12 @@ export class Session {
telemetry: this.telemetry,
tokenSource: this.state.tokenSource,
uploadHistory: this.state.uploadHistory,
- tokenSource: this.state.tokenSource,
})
if (resp.nextState) {
- // Cancel the request before moving to a new state
-<<<<<<< HEAD
-<<<<<<< HEAD
if (!this.state?.tokenSource?.token.isCancellationRequested) {
this.state?.tokenSource?.cancel()
}
-=======
- this.state?.tokenSource?.cancel()
->>>>>>> 4742b3c59 (refactor(amazonqFeatureDev): include updated rts model)
-=======
- if (!this.state?.tokenSource?.token.isCancellationRequested) this.state?.tokenSource?.cancel()
->>>>>>> e6782b66f (fix(amazonqFeatureDev): create a new token to stop and iterate in the same session)
-
// Move to the next state
this._state = resp.nextState
}
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index 33e71ee46c7..2926fb86a21 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -535,11 +535,7 @@ export class PrepareCodeGenState implements SessionState {
span
)
const uploadId = randomUUID()
- const {
- uploadUrl,
- uploadId: returnedUploadId,
- kmsKeyArn,
- } = await this.config.proxyClient.createUploadUrl(
+ const { uploadUrl, kmsKeyArn } = await this.config.proxyClient.createUploadUrl(
this.config.conversationId,
zipFileChecksum,
zipFileBuffer.length,
From 3cfaf44999836deb390dacfc2ebd5245abc507a5 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Thu, 26 Sep 2024 15:12:15 -0400
Subject: [PATCH 79/87] fix(amazonqFeatureDev): use shared context from action
to check cancellation
---
.../amazonqFeatureDev/session/sessionState.ts | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index 2926fb86a21..838d0a2c32f 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -147,7 +147,6 @@ abstract class CodeGenBase {
public tabID: string
) {
this.tokenSource = new vscode.CancellationTokenSource()
- this.isCancellationRequested = false
this.conversationId = config.conversationId
this.uploadId = config.uploadId
this.currentCodeGenerationId = config.currentCodeGenerationId || EmptyCodeGenID
@@ -159,12 +158,14 @@ abstract class CodeGenBase {
codeGenerationId,
telemetry: telemetry,
workspaceFolders,
+ isCancellationRequested,
}: {
messenger: Messenger
fs: VirtualFileSystem
codeGenerationId: string
telemetry: TelemetryHelper
workspaceFolders: CurrentWsFolders
+ isCancellationRequested?: boolean
}): Promise<{
newFiles: NewFileInfo[]
deletedFiles: DeletedFileInfo[]
@@ -174,7 +175,7 @@ abstract class CodeGenBase {
}> {
for (
let pollingIteration = 0;
- pollingIteration < this.pollCount && !this.isCancellationRequested;
+ pollingIteration < this.pollCount && !isCancellationRequested;
++pollingIteration
) {
const codegenResult = await this.config.proxyClient.getCodeGeneration(this.conversationId, codeGenerationId)
@@ -259,7 +260,7 @@ abstract class CodeGenBase {
}
}
}
- if (!this.isCancellationRequested) {
+ if (!isCancellationRequested) {
// still in progress
const errorMessage = i18n('AWS.amazonq.featureDev.error.codeGen.timeout')
throw new ToolkitError(errorMessage, { code: 'CodeGenTimeout' })
@@ -302,12 +303,10 @@ export class CodeGenState extends CodeGenBase implements SessionState {
})
action.tokenSource?.token.onCancellationRequested(() => {
+ this.isCancellationRequested = true
if (action.tokenSource) {
this.tokenSource = action.tokenSource
}
- this.isCancellationRequested = true
- action.tokenSource?.dispose()
- action.tokenSource = undefined
})
action.telemetry.setGenerateCodeIteration(this.currentIteration)
@@ -321,7 +320,7 @@ export class CodeGenState extends CodeGenBase implements SessionState {
this.currentCodeGenerationId
)
- if (!this.isCancellationRequested) {
+ if (!action.tokenSource?.token.isCancellationRequested) {
action.messenger.sendAnswer({
message: i18n('AWS.amazonq.featureDev.pillText.generatingCode'),
type: 'answer-part',
@@ -339,6 +338,7 @@ export class CodeGenState extends CodeGenBase implements SessionState {
codeGenerationId,
telemetry: action.telemetry,
workspaceFolders: this.config.workspaceFolders,
+ isCancellationRequested: action.tokenSource?.token.isCancellationRequested,
})
if (codeGeneration && !action.tokenSource?.token.isCancellationRequested) {
@@ -375,7 +375,7 @@ export class CodeGenState extends CodeGenBase implements SessionState {
this.codeGenerationTotalIterationCount,
action.uploadHistory,
codeGenerationId,
- this.tokenSource,
+ action.tokenSource,
this.currentCodeGenerationId
)
return {
From 1fd35afef96a32ccd905db2eb56134d45df7b0d8 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Thu, 26 Sep 2024 15:53:17 -0400
Subject: [PATCH 80/87] fix(amazonqFeatureDev): require isCancellation to
localize in the instance and abort the individual call
---
.../core/src/amazonqFeatureDev/session/sessionState.ts | 9 +++------
1 file changed, 3 insertions(+), 6 deletions(-)
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index 838d0a2c32f..8b005fad580 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -158,14 +158,12 @@ abstract class CodeGenBase {
codeGenerationId,
telemetry: telemetry,
workspaceFolders,
- isCancellationRequested,
}: {
messenger: Messenger
fs: VirtualFileSystem
codeGenerationId: string
telemetry: TelemetryHelper
workspaceFolders: CurrentWsFolders
- isCancellationRequested?: boolean
}): Promise<{
newFiles: NewFileInfo[]
deletedFiles: DeletedFileInfo[]
@@ -175,7 +173,7 @@ abstract class CodeGenBase {
}> {
for (
let pollingIteration = 0;
- pollingIteration < this.pollCount && !isCancellationRequested;
+ pollingIteration < this.pollCount && !this.isCancellationRequested;
++pollingIteration
) {
const codegenResult = await this.config.proxyClient.getCodeGeneration(this.conversationId, codeGenerationId)
@@ -260,7 +258,7 @@ abstract class CodeGenBase {
}
}
}
- if (!isCancellationRequested) {
+ if (!this.isCancellationRequested) {
// still in progress
const errorMessage = i18n('AWS.amazonq.featureDev.error.codeGen.timeout')
throw new ToolkitError(errorMessage, { code: 'CodeGenTimeout' })
@@ -320,7 +318,7 @@ export class CodeGenState extends CodeGenBase implements SessionState {
this.currentCodeGenerationId
)
- if (!action.tokenSource?.token.isCancellationRequested) {
+ if (!this.isCancellationRequested) {
action.messenger.sendAnswer({
message: i18n('AWS.amazonq.featureDev.pillText.generatingCode'),
type: 'answer-part',
@@ -338,7 +336,6 @@ export class CodeGenState extends CodeGenBase implements SessionState {
codeGenerationId,
telemetry: action.telemetry,
workspaceFolders: this.config.workspaceFolders,
- isCancellationRequested: action.tokenSource?.token.isCancellationRequested,
})
if (codeGeneration && !action.tokenSource?.token.isCancellationRequested) {
From 597a92d448d28240665c0fd5b821d7c6122a2d36 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Wed, 2 Oct 2024 15:58:52 -0400
Subject: [PATCH 81/87] fix(amazonqFeatureDev): don't share state to prepare
code since code cancellation should be in gen
---
packages/core/src/amazonqFeatureDev/session/sessionState.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index 8b005fad580..ae44dd42e9c 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -372,7 +372,7 @@ export class CodeGenState extends CodeGenBase implements SessionState {
this.codeGenerationTotalIterationCount,
action.uploadHistory,
codeGenerationId,
- action.tokenSource,
+ this.tokenSource,
this.currentCodeGenerationId
)
return {
From f45a4bbc13267d42bbb963f4ee99687e7bfc271d Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Tue, 15 Oct 2024 15:23:27 -0400
Subject: [PATCH 82/87] fix(dev): merge conflicts and missing commas
---
.../core/src/amazonqFeatureDev/session/sessionState.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index ae44dd42e9c..3e5d8a5633e 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -371,9 +371,9 @@ export class CodeGenState extends CodeGenBase implements SessionState {
this.codeGenerationRemainingIterationCount,
this.codeGenerationTotalIterationCount,
action.uploadHistory,
- codeGenerationId,
this.tokenSource,
- this.currentCodeGenerationId
+ this.currentCodeGenerationId,
+ codeGenerationId
)
return {
nextState,
@@ -496,9 +496,9 @@ export class PrepareCodeGenState implements SessionState {
public codeGenerationRemainingIterationCount?: number,
public codeGenerationTotalIterationCount?: number,
public uploadHistory: UploadHistory = {},
- public codeGenerationId?: string,
public superTokenSource?: vscode.CancellationTokenSource,
- public currentCodeGenerationId?: string
+ public currentCodeGenerationId?: string,
+ public codeGenerationId?: string
) {
this.tokenSource = superTokenSource || new vscode.CancellationTokenSource()
this.uploadId = config.uploadId
From 67d1958e2f101bed6990895af2e85c21e9f62c2e Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Mon, 21 Oct 2024 15:13:46 -0400
Subject: [PATCH 83/87] fix(dev): fix merge conflicts
---
packages/core/src/amazonqFeatureDev/client/featureDev.ts | 7 -------
1 file changed, 7 deletions(-)
diff --git a/packages/core/src/amazonqFeatureDev/client/featureDev.ts b/packages/core/src/amazonqFeatureDev/client/featureDev.ts
index 374e8b17949..8cd5ee97f90 100644
--- a/packages/core/src/amazonqFeatureDev/client/featureDev.ts
+++ b/packages/core/src/amazonqFeatureDev/client/featureDev.ts
@@ -134,14 +134,7 @@ export class FeatureDevClient {
conversationId: string,
uploadId: string,
message: string,
-<<<<<<< HEAD
-<<<<<<< HEAD
codeGenerationId: string,
-=======
->>>>>>> 4742b3c59 (refactor(amazonqFeatureDev): include updated rts model)
-=======
- codeGenerationId: string,
->>>>>>> d9c61d527 (refactor(amazonqFeatureDev): add logic for codeGenerationId)
currentCodeGenerationId?: string
) {
try {
From 14593a95097938484bc1d211ae28326c6b28405c Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Tue, 22 Oct 2024 13:50:21 -0400
Subject: [PATCH 84/87] fix(dev): update messaging for total iterations
---
.../amazonqFeatureDev/controllers/chat/controller.ts | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
index bf66fe77a4b..3640ab98910 100644
--- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
+++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
@@ -47,6 +47,8 @@ import { openDeletedDiff, openDiff } from '../../../amazonq/commons/diff'
import { i18n } from '../../../shared/i18n-helper'
import globals from '../../../shared/extensionGlobals'
+export const TotalSteps = 3
+
export interface ChatControllerEventEmitters {
readonly processHumanChatMessage: EventEmitter
readonly followUpClicked: EventEmitter
@@ -454,8 +456,9 @@ export class FeatureDevController {
if (session?.state?.tokenSource?.token.isCancellationRequested) {
this.workOnNewTask(
session,
- session.state.codeGenerationRemainingIterationCount || session.state?.currentIteration,
- session.state.codeGenerationTotalIterationCount,
+ session.state.codeGenerationRemainingIterationCount ||
+ TotalSteps - (session.state?.currentIteration || 0),
+ session.state.codeGenerationTotalIterationCount || TotalSteps,
session?.state?.tokenSource?.token.isCancellationRequested
)
this.disposeToken(session)
@@ -490,7 +493,7 @@ export class FeatureDevController {
message:
remainingIterations === 0
? "I stopped generating your code. You don't have more iterations left, however, you can start a new session."
- : `I stopped generating your code. If you want to continue working on this task, provide another description. ${!totalIterations ? `You have started ${remainingIterations} code generations.` : `You have ${remainingIterations} out of ${totalIterations} code generations left.`}`,
+ : `I stopped generating your code. If you want to continue working on this task, provide another description. You have ${remainingIterations} out of ${totalIterations} code generations left.`,
type: 'answer-part',
tabID: message.tabID,
})
@@ -513,6 +516,9 @@ export class FeatureDevController {
},
],
})
+ this.messenger.sendChatInputEnabled(message.tabID, false)
+ this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.featureDev.pillText.selectOption'))
+ return
}
// Ensure that chat input is enabled so that they can provide additional iterations if they choose
From 3bafa50d77a7eb1b5fb761bf8ae4c78849d0cb27 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Tue, 22 Oct 2024 15:38:01 -0400
Subject: [PATCH 85/87] fix(dev): safe check for remainingIterations
---
.../core/src/amazonqFeatureDev/controllers/chat/controller.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
index 3640ab98910..d9afd9dd831 100644
--- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
+++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
@@ -491,7 +491,7 @@ export class FeatureDevController {
if (isStoppedGeneration) {
this.messenger.sendAnswer({
message:
- remainingIterations === 0
+ (remainingIterations ?? 0) <= 0
? "I stopped generating your code. You don't have more iterations left, however, you can start a new session."
: `I stopped generating your code. If you want to continue working on this task, provide another description. You have ${remainingIterations} out of ${totalIterations} code generations left.`,
type: 'answer-part',
@@ -499,7 +499,7 @@ export class FeatureDevController {
})
}
- if ((remainingIterations === 0 && isStoppedGeneration) || !isStoppedGeneration) {
+ if (((remainingIterations ?? 0) <= 0 && isStoppedGeneration) || !isStoppedGeneration) {
this.messenger.sendAnswer({
type: 'system-prompt',
tabID: message.tabID,
From e8dddc14e778c79158bed254ed9f73d8ba418b13 Mon Sep 17 00:00:00 2001
From: Thiago Verney
Date: Wed, 23 Oct 2024 11:13:52 -0400
Subject: [PATCH 86/87] fix(dev): apply feedback
---
packages/core/src/amazonq/webview/ui/connector.ts | 9 +++++++--
.../controllers/chat/controller.ts | 13 +++++++++++--
.../src/amazonqFeatureDev/session/sessionState.ts | 9 +--------
3 files changed, 19 insertions(+), 12 deletions(-)
diff --git a/packages/core/src/amazonq/webview/ui/connector.ts b/packages/core/src/amazonq/webview/ui/connector.ts
index 0f4aa82652c..1d832c8e023 100644
--- a/packages/core/src/amazonq/webview/ui/connector.ts
+++ b/packages/core/src/amazonq/webview/ui/connector.ts
@@ -177,8 +177,13 @@ export class Connector {
}
onStopChatResponse = (tabID: string): void => {
- if (this.tabsStorage.getTab(tabID)?.type === 'featuredev') {
- this.featureDevChatConnector.onStopChatResponse(tabID)
+ switch (this.tabsStorage.getTab(tabID)?.type) {
+ case 'featuredev':
+ this.featureDevChatConnector.onStopChatResponse(tabID)
+ break
+ case 'cwc':
+ this.cwChatConnector.onStopChatResponse(tabID)
+ break
}
}
diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
index d9afd9dd831..efb26e9b7cb 100644
--- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
+++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
@@ -313,6 +313,15 @@ export class FeatureDevController {
}
}
+ /**
+ *
+ * This function dispose cancellation token to free resources and provide a new token.
+ * Since user can abort a call in the same session, when the processing ends, we need provide a new one
+ * to start with the new prompt and allow the ability to stop again.
+ *
+ * @param session
+ */
+
private disposeToken(session: Session | undefined) {
if (session?.state?.tokenSource?.token.isCancellationRequested) {
session?.state.tokenSource?.dispose()
@@ -484,7 +493,7 @@ export class FeatureDevController {
}
private workOnNewTask(
message: any,
- remainingIterations?: number,
+ remainingIterations: number = 0,
totalIterations?: number,
isStoppedGeneration: boolean = false
) {
@@ -499,7 +508,7 @@ export class FeatureDevController {
})
}
- if (((remainingIterations ?? 0) <= 0 && isStoppedGeneration) || !isStoppedGeneration) {
+ if ((remainingIterations <= 0 && isStoppedGeneration) || !isStoppedGeneration) {
this.messenger.sendAnswer({
type: 'system-prompt',
tabID: message.tabID,
diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
index 3e5d8a5633e..bc1a6f0e35b 100644
--- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts
+++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts
@@ -300,13 +300,6 @@ export class CodeGenState extends CodeGenBase implements SessionState {
credentialStartUrl: AuthUtil.instance.startUrl,
})
- action.tokenSource?.token.onCancellationRequested(() => {
- this.isCancellationRequested = true
- if (action.tokenSource) {
- this.tokenSource = action.tokenSource
- }
- })
-
action.telemetry.setGenerateCodeIteration(this.currentIteration)
action.telemetry.setGenerateCodeLastInvocationTime()
const codeGenerationId = randomUUID()
@@ -496,7 +489,7 @@ export class PrepareCodeGenState implements SessionState {
public codeGenerationRemainingIterationCount?: number,
public codeGenerationTotalIterationCount?: number,
public uploadHistory: UploadHistory = {},
- public superTokenSource?: vscode.CancellationTokenSource,
+ public superTokenSource: vscode.CancellationTokenSource = new vscode.CancellationTokenSource(),
public currentCodeGenerationId?: string,
public codeGenerationId?: string
) {
From e6c1e4fe86a01a65f6ab994c6a27af01cdab1649 Mon Sep 17 00:00:00 2001
From: Kelvin Chu
Date: Wed, 23 Oct 2024 15:47:45 -0400
Subject: [PATCH 87/87] telemetry(amazonq): use ui_click telemetry for stop
code generate button instead
---
.../controllers/chat/controller.ts | 32 +++++++++----------
.../src/shared/telemetry/vscodeTelemetry.json | 15 ---------
.../controllers/chat/controller.test.ts | 7 ++--
3 files changed, 17 insertions(+), 37 deletions(-)
diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
index efb26e9b7cb..aa300b9ddba 100644
--- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
+++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts
@@ -788,24 +788,22 @@ export class FeatureDevController {
}
private async stopResponse(message: any) {
- await telemetry.amazonq_stopCodeGeneration.run(async (span) => {
- span.record({ tabID: message.tabID })
- this.messenger.sendAnswer({
- message: i18n('AWS.amazonq.featureDev.pillText.stoppingCodeGeneration'),
- type: 'answer-part',
- tabID: message.tabID,
- })
- this.messenger.sendUpdatePlaceholder(
- message.tabID,
- i18n('AWS.amazonq.featureDev.pillText.stoppingCodeGeneration')
- )
- this.messenger.sendChatInputEnabled(message.tabID, false)
-
- const session = await this.sessionStorage.getSession(message.tabID)
- if (session.state?.tokenSource) {
- session.state?.tokenSource?.cancel()
- }
+ telemetry.ui_click.emit({ elementId: 'amazonq_stopCodeGeneration' })
+ this.messenger.sendAnswer({
+ message: i18n('AWS.amazonq.featureDev.pillText.stoppingCodeGeneration'),
+ type: 'answer-part',
+ tabID: message.tabID,
})
+ this.messenger.sendUpdatePlaceholder(
+ message.tabID,
+ i18n('AWS.amazonq.featureDev.pillText.stoppingCodeGeneration')
+ )
+ this.messenger.sendChatInputEnabled(message.tabID, false)
+
+ const session = await this.sessionStorage.getSession(message.tabID)
+ if (session.state?.tokenSource) {
+ session.state?.tokenSource?.cancel()
+ }
}
private async tabOpened(message: any) {
diff --git a/packages/core/src/shared/telemetry/vscodeTelemetry.json b/packages/core/src/shared/telemetry/vscodeTelemetry.json
index a82c949e06c..9c3dbbdfd67 100644
--- a/packages/core/src/shared/telemetry/vscodeTelemetry.json
+++ b/packages/core/src/shared/telemetry/vscodeTelemetry.json
@@ -335,11 +335,6 @@
"name": "amazonqMessageDisplayedMs",
"type": "int",
"description": "Duration between the partner teams code receiving the message and when the message was finally displayed in ms"
- },
- {
- "name": "tabID",
- "type": "string",
- "description": "The unique identifier of a tab"
}
],
"metrics": [
@@ -1191,16 +1186,6 @@
"required": false
}
]
- },
- {
- "name": "amazonq_stopCodeGeneration",
- "description": "User stopped the code generation",
- "metadata": [
- {
- "type": "tabID",
- "required": true
- }
- ]
}
]
}
diff --git a/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts b/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts
index 9c159783625..e7e8ecdb6fc 100644
--- a/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts
+++ b/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts
@@ -510,16 +510,13 @@ describe('Controller', () => {
})
describe('stopResponse', () => {
- it('should emit amazonq_stopCodeGeneration telemetry', async () => {
+ it('should emit ui_click telemetry with elementId amazonq_stopCodeGeneration', async () => {
const getSessionStub = sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session)
controllerSetup.emitters.stopResponse.fire({ tabID, conversationID })
await waitUntil(() => {
return Promise.resolve(getSessionStub.callCount > 0)
}, {})
- assertTelemetry('amazonq_stopCodeGeneration', {
- tabID: tabID,
- result: 'Succeeded',
- })
+ assertTelemetry('ui_click', { elementId: 'amazonq_stopCodeGeneration' })
})
})
})