Skip to content

Commit

Permalink
UTG Error handling rebasing
Browse files Browse the repository at this point in the history
  • Loading branch information
laileni-aws authored and ashishrp-aws committed Jan 16, 2025
1 parent 62ef0e5 commit 6c25777
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 83 deletions.
103 changes: 58 additions & 45 deletions packages/core/src/amazonqTest/chat/controller/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
TelemetryHelper,
TestGenerationBuildStep,
testGenState,
tooManyRequestErrorMessage,
unitTestGenerationCancelMessage,
UserWrittenCodeTracker,
} from '../../../codewhisperer'
Expand Down Expand Up @@ -242,72 +243,76 @@ export class TestController {
// eslint-disable-next-line unicorn/no-null
this.messenger.sendUpdatePromptProgress(data.tabID, null)
const session = this.sessionStorage.getSession()
const isCancel = data.error.message === unitTestGenerationCancelMessage

const isCancel = data.error.uiMessage === unitTestGenerationCancelMessage
let telemetryErrorMessage = getTelemetryReasonDesc(data.error)
if (session.stopIteration) {
telemetryErrorMessage = getTelemetryReasonDesc(data.error.uiMessage.replaceAll('```', ''))
}
TelemetryHelper.instance.sendTestGenerationToolkitEvent(
session,
true,
true,
isCancel ? 'Cancelled' : 'Failed',
session.startTestGenerationRequestId,
performance.now() - session.testGenerationStartTime,
getTelemetryReasonDesc(data.error),
telemetryErrorMessage,
session.isCodeBlockSelected,
session.artifactsUploadDuration,
session.srcPayloadSize,
session.srcZipFileSize
)

if (session.stopIteration) {
// Error from Science
this.messenger.sendMessage(data.error.message.replaceAll('```', ''), data.tabID, 'answer')
this.messenger.sendMessage(data.error.uiMessage.replaceAll('```', ''), data.tabID, 'answer')
} else {
isCancel
? this.messenger.sendMessage(data.error.message, data.tabID, 'answer')
? this.messenger.sendMessage(data.error.uiMessage, data.tabID, 'answer')
: this.sendErrorMessage(data)
}
await this.sessionCleanUp()
return
}
// Client side error messages
private sendErrorMessage(data: { tabID: string; error: { code: string; message: string } }) {
private sendErrorMessage(data: {
tabID: string
error: { uiMessage: string; message: string; code: string; statusCode: string }
}) {
const { error, tabID } = data

// If user reached monthly limit for builderId
if (error.code === 'CreateTestJobError') {
if (error.message.includes(CodeWhispererConstants.utgLimitReached)) {
getLogger().error('Monthly quota reached for QSDA actions.')
return this.messenger.sendMessage(
i18n('AWS.amazonq.featureDev.error.monthlyLimitReached'),
tabID,
'answer'
)
}
if (error.message.includes('Too many requests')) {
getLogger().error(error.message)
return this.messenger.sendErrorMessage(tooManyRequestErrorMessage, tabID)
}
}
if (isAwsError(error)) {
if (error.code === 'ThrottlingException') {
// TODO: use the explicitly modeled exception reason for quota vs throttle
if (error.message.includes(CodeWhispererConstants.utgLimitReached)) {
getLogger().error('Monthly quota reached for QSDA actions.')
return this.messenger.sendMessage(
i18n('AWS.amazonq.featureDev.error.monthlyLimitReached'),
tabID,
'answer'
)
} else {
getLogger().error('Too many requests.')
// TODO: move to constants file
this.messenger.sendErrorMessage('Too many requests. Please wait before retrying.', tabID)
}
} else {
// other service errors:
// AccessDeniedException - should not happen because access is validated before this point in the client
// ValidationException - shouldn't happen because client should not send malformed requests
// ConflictException - should not happen because the client will maintain proper state
// InternalServerException - shouldn't happen but needs to be caught
getLogger().error('Other error message: %s', error.message)
this.messenger.sendErrorMessage(
'Encountered an unexpected error when generating tests. Please try again',
tabID
)
// TODO: use the explicitly modeled exception reason for quota vs throttle{
getLogger().error(error.message)
this.messenger.sendErrorMessage(tooManyRequestErrorMessage, tabID)
return
}
} else {
// other unexpected errors (TODO enumerate all other failure cases)
// other service errors:
// AccessDeniedException - should not happen because access is validated before this point in the client
// ValidationException - shouldn't happen because client should not send malformed requests
// ConflictException - should not happen because the client will maintain proper state
// InternalServerException - shouldn't happen but needs to be caught
getLogger().error('Other error message: %s', error.message)
this.messenger.sendErrorMessage(
'Encountered an unexpected error when generating tests. Please try again',
tabID
)
this.messenger.sendErrorMessage('', tabID)
return
}
// other unexpected errors (TODO enumerate all other failure cases)
getLogger().error('Other error message: %s', error.uiMessage)
this.messenger.sendErrorMessage('', tabID)
}

// This function handles actions if user clicked on any Button one of these cases will be executed
Expand Down Expand Up @@ -730,6 +735,9 @@ export class TestController {
// this.messenger.sendMessage('Accepted', message.tabID, 'prompt')
telemetry.ui_click.emit({ elementId: 'unitTestGeneration_acceptDiff' })

getLogger().info(
`Generated unit tests are accepted for ${session.fileLanguage ?? 'plaintext'} language with jobId: ${session.listOfTestGenerationJobId[0]}, jobGroupName: ${session.testGenerationJobGroupName}, result: Succeeded`
)
TelemetryHelper.instance.sendTestGenerationToolkitEvent(
session,
true,
Expand All @@ -751,7 +759,6 @@ export class TestController {
)

await this.endSession(message, FollowUpTypes.SkipBuildAndFinish)
await this.sessionCleanUp()
return

if (session.listOfTestGenerationJobId.length === 1) {
Expand Down Expand Up @@ -876,16 +883,12 @@ export class TestController {
session.numberOfTestsGenerated,
session.linesOfCodeGenerated
)

telemetry.ui_click.emit({ elementId: 'unitTestGeneration_rejectDiff' })
}

await this.sessionCleanUp()
// TODO: revert 'Accepted' to 'Skip build and finish' once supported
const message = step === FollowUpTypes.RejectCode ? 'Rejected' : 'Accepted'

this.messenger.sendMessage(message, data.tabID, 'prompt')
this.messenger.sendMessage(`Unit test generation workflow is completed.`, data.tabID, 'answer')
// this.messenger.sendMessage(`Unit test generation workflow is completed.`, data.tabID, 'answer')
this.messenger.sendChatInputEnabled(data.tabID, true)
return
}
Expand Down Expand Up @@ -1320,8 +1323,18 @@ export class TestController {
'Deleting output.log and temp result directory. testGenerationLogsDir: %s',
testGenerationLogsDir
)
await fs.delete(path.join(testGenerationLogsDir, 'output.log'))
await fs.delete(this.tempResultDirPath, { recursive: true })
const outputLogPath = path.join(testGenerationLogsDir, 'output.log')
if (await fs.existsFile(outputLogPath)) {
await fs.delete(outputLogPath)
}
if (
await fs
.stat(this.tempResultDirPath)
.then(() => true)
.catch(() => false)
) {
await fs.delete(this.tempResultDirPath, { recursive: true })
}
}

// TODO: return build command when product approves
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,9 +282,21 @@ export class Messenger {
'Cancelled',
messageId,
performance.now() - session.testGenerationStartTime,
getTelemetryReasonDesc(CodeWhispererConstants.unitTestGenerationCancelMessage)
getTelemetryReasonDesc(
`TestGenCancelled: ${CodeWhispererConstants.unitTestGenerationCancelMessage}`
),
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
'TestGenCancelled'
)

this.dispatcher.sendUpdatePromptProgress(
new UpdatePromptProgressMessage(tabID, cancellingProgressField)
)
Expand All @@ -296,9 +308,9 @@ export class Messenger {
fileInWorkspace,
'Succeeded',
messageId,
performance.now() - session.testGenerationStartTime
performance.now() - session.testGenerationStartTime,
undefined
)

this.dispatcher.sendUpdatePromptProgress(
new UpdatePromptProgressMessage(tabID, testGenCompletedField)
)
Expand Down
67 changes: 67 additions & 0 deletions packages/core/src/amazonqTest/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import { ToolkitError } from '../shared/errors'

export const technicalErrorCustomerFacingMessage =
'I am experiencing technical difficulties at the moment. Please try again in a few minutes.'
const defaultTestGenErrorMessage = 'Amazon Q encountered an error while generating tests. Try again later.'
export class TestGenError extends ToolkitError {
constructor(
error: string,
code: string,
public uiMessage: string
) {
super(error, { code })
}
}
export class ProjectZipError extends TestGenError {
constructor(error: string) {
super(error, 'ProjectZipError', defaultTestGenErrorMessage)
}
}
export class InvalidSourceZipError extends TestGenError {
constructor() {
super('Failed to create valid source zip', 'InvalidSourceZipError', defaultTestGenErrorMessage)
}
}
export class CreateUploadUrlError extends TestGenError {
constructor(errorMessage: string) {
super(errorMessage, 'CreateUploadUrlError', technicalErrorCustomerFacingMessage)
}
}
export class UploadTestArtifactToS3Error extends TestGenError {
constructor(error: string) {
super(error, 'UploadTestArtifactToS3Error', technicalErrorCustomerFacingMessage)
}
}
export class CreateTestJobError extends TestGenError {
constructor(error: string) {
super(error, 'CreateTestJobError', technicalErrorCustomerFacingMessage)
}
}
export class TestGenTimedOutError extends TestGenError {
constructor() {
super(
'Test generation failed. Amazon Q timed out.',
'TestGenTimedOutError',
technicalErrorCustomerFacingMessage
)
}
}
export class TestGenStoppedError extends TestGenError {
constructor() {
super('Unit test generation cancelled.', 'TestGenCancelled', 'Unit test generation cancelled.')
}
}
export class TestGenFailedError extends TestGenError {
constructor(error?: string) {
super(error ?? 'Test generation failed', 'TestGenFailedError', error ?? technicalErrorCustomerFacingMessage)
}
}
export class ExportResultsArchiveError extends TestGenError {
constructor(error?: string) {
super(error ?? 'Test generation failed', 'ExportResultsArchiveError', technicalErrorCustomerFacingMessage)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { ChildProcess, spawn } from 'child_process' // eslint-disable-line no-re
import { BuildStatus } from '../../amazonqTest/chat/session/session'
import { fs } from '../../shared/fs/fs'
import { TestGenerationJobStatus } from '../models/constants'
import { TestGenFailedError } from '../models/errors'
import { TestGenFailedError } from '../../amazonqTest/error'
import { Range } from '../client/codewhispereruserclient'

// eslint-disable-next-line unicorn/no-null
Expand Down Expand Up @@ -75,8 +75,9 @@ export async function startTestGenerationProcess(
try {
artifactMap = await getPresignedUrlAndUploadTestGen(zipMetadata)
} finally {
if (await fs.existsFile(path.join(testGenerationLogsDir, 'output.log'))) {
await fs.delete(path.join(testGenerationLogsDir, 'output.log'))
const outputLogPath = path.join(testGenerationLogsDir, 'output.log')
if (await fs.existsFile(outputLogPath)) {
await fs.delete(outputLogPath)
}
await zipUtil.removeTmpFiles(zipMetadata)
session.artifactsUploadDuration = performance.now() - uploadStartTime
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/codewhisperer/models/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,8 @@ export const noOpenProjectsFoundChatTestGenMessage = `Sorry, I couldn\'t find a

export const unitTestGenerationCancelMessage = 'Unit test generation cancelled.'

export const tooManyRequestErrorMessage = 'Too many requests. Please wait before retrying.'

export const noJavaProjectsFoundChatMessage = `I couldn\'t find a project that I can upgrade. Currently, I support Java 8, Java 11, and Java 17 projects built on Maven. Make sure your project is open in the IDE. For more information, see the [Amazon Q documentation](${codeTransformPrereqDoc}).`

export const linkToDocsHome = 'https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/code-transformation.html'
Expand Down Expand Up @@ -861,7 +863,7 @@ export enum TestGenerationJobStatus {
COMPLETED = 'COMPLETED',
}

export enum ZipUseCase {
export enum FeatureUseCase {
TEST_GENERATION = 'TEST_GENERATION',
CODE_SCAN = 'CODE_SCAN',
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/codewhisperer/service/codeFixHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export async function getPresignedUrlAndUpload(
getLogger().verbose(`CreateUploadUrlRequest requestId: ${srcResp.$response.requestId}`)
getLogger().verbose(`Complete Getting presigned Url for uploading src context.`)
getLogger().verbose(`Uploading src context...`)
await uploadArtifactToS3(zipFilePath, srcResp)
await uploadArtifactToS3(zipFilePath, srcResp, CodeWhispererConstants.FeatureUseCase.CODE_SCAN)
getLogger().verbose(`Complete uploading src context.`)
const artifactMap: ArtifactMap = {
SourceCode: srcResp.uploadId,
Expand Down
26 changes: 19 additions & 7 deletions packages/core/src/codewhisperer/service/securityScanHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import { getTelemetryReasonDesc } from '../../shared/errors'
import { CodeWhispererSettings } from '../util/codewhispererSettings'
import { detectCommentAboveLine } from '../../shared/utilities/commentUtils'
import { runtimeLanguageContext } from '../util/runtimeLanguageContext'
import { FeatureUseCase } from '../models/constants'
import { UploadTestArtifactToS3Error } from '../../amazonqTest/error'

export async function listScanResults(
client: DefaultCodeWhispererClient,
Expand Down Expand Up @@ -287,7 +289,7 @@ export async function getPresignedUrlAndUpload(
logger.verbose(`CreateUploadUrlRequest request id: ${srcResp.$response.requestId}`)
logger.verbose(`Complete Getting presigned Url for uploading src context.`)
logger.verbose(`Uploading src context...`)
await uploadArtifactToS3(zipMetadata.zipFilePath, srcResp, scope)
await uploadArtifactToS3(zipMetadata.zipFilePath, srcResp, FeatureUseCase.CODE_SCAN, scope)
logger.verbose(`Complete uploading src context.`)
const artifactMap: ArtifactMap = {
SourceCode: srcResp.uploadId,
Expand Down Expand Up @@ -343,6 +345,7 @@ export function throwIfCancelled(scope: CodeWhispererConstants.CodeAnalysisScope
export async function uploadArtifactToS3(
fileName: string,
resp: CreateUploadUrlResponse,
featureUseCase: FeatureUseCase,
scope?: CodeWhispererConstants.CodeAnalysisScope
) {
const logger = getLoggerForScope(scope)
Expand All @@ -365,14 +368,23 @@ export async function uploadArtifactToS3(
}).response
logger.debug(`StatusCode: ${response.status}, Text: ${response.statusText}`)
} catch (error) {
let errorMessage = ''
const isCodeScan = featureUseCase === FeatureUseCase.CODE_SCAN
const featureType = isCodeScan ? 'security scans' : 'unit test generation'
const defaultMessage = isCodeScan ? 'Security scan failed.' : 'Test generation failed.'
getLogger().error(
`Amazon Q is unable to upload workspace artifacts to Amazon S3 for security scans. For more information, see the Amazon Q documentation or contact your network or organization administrator.`
`Amazon Q is unable to upload workspace artifacts to Amazon S3 for ${featureType}. ` +
'For more information, see the Amazon Q documentation or contact your network or organization administrator.'
)
const errorMessage = getTelemetryReasonDesc(error)?.includes(`"PUT" request failed with code "403"`)
? `"PUT" request failed with code "403"`
: (getTelemetryReasonDesc(error) ?? 'Security scan failed.')

throw new UploadArtifactToS3Error(errorMessage)
const errorDesc = getTelemetryReasonDesc(error)
if (errorDesc?.includes('"PUT" request failed with code "403"')) {
errorMessage = '"PUT" request failed with code "403"'
} else if (errorDesc?.includes('"PUT" request failed with code "503"')) {
errorMessage = '"PUT" request failed with code "503"'
} else {
errorMessage = errorDesc ?? defaultMessage
}
throw isCodeScan ? new UploadArtifactToS3Error(errorMessage) : new UploadTestArtifactToS3Error(errorMessage)
}
}

Expand Down
Loading

0 comments on commit 6c25777

Please sign in to comment.