Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

telemetry(amazonq): Improving error handling and telemetry in unit test generation. #6187

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 69 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,
} from '../../../codewhisperer'
import {
Expand Down Expand Up @@ -241,71 +242,84 @@ 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,
isCancel ? 'Cancelled' : 'Failed',
session.startTestGenerationRequestId,
performance.now() - session.testGenerationStartTime,
getTelemetryReasonDesc(data.error),
telemetryErrorMessage,
data.error.statusCode ?? '0',
session.isCodeBlockSelected,
session.artifactsUploadDuration,
session.srcPayloadSize,
session.srcZipFileSize
session.srcZipFileSize,
session.charsOfCodeAccepted,
session.numberOfTestsGenerated,
session.linesOfCodeAccepted,
session.charsOfCodeGenerated,
session.numberOfTestsGenerated,
session.linesOfCodeGenerated,
data.error.code
)

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'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is using featureDev message, is that intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes its intentional, Its a generic message for Q agent.

"AWS.amazonq.featureDev.error.monthlyLimitReached": "You've reached the monthly quota for Amazon Q Developer's agent capabilities. You can try again next month. For more information on usage limits, see the <a href=\"https://aws.amazon.com/q/developer/pricing/\" target=\"_blank\">Amazon Q Developer pricing page</a>.",

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 @@ -714,13 +728,17 @@ 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,
'Succeeded',
session.startTestGenerationRequestId,
session.latencyOfTestGeneration,
undefined,
'200',
session.isCodeBlockSelected,
session.artifactsUploadDuration,
session.srcPayloadSize,
Expand All @@ -734,7 +752,6 @@ export class TestController {
)

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

if (session.listOfTestGenerationJobId.length === 1) {
Expand Down Expand Up @@ -842,6 +859,7 @@ export class TestController {
session.startTestGenerationRequestId,
session.latencyOfTestGeneration,
undefined,
'200',
session.isCodeBlockSelected,
session.artifactsUploadDuration,
session.srcPayloadSize,
Expand All @@ -853,16 +871,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')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Existing public plugin does not reach this part of code as IDE throws an error at sessionCleanUp

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that you fixed it, do we still want to comment this out?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to check whether we need to have this text after Accept or Reject action?
As JB does show this text after Accept/Reject but VSC don't.
We can add this accordingly.

this.messenger.sendChatInputEnabled(data.tabID, true)
return
}
Expand Down Expand Up @@ -1297,8 +1311,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 @@ -280,9 +280,22 @@ export class Messenger {
'Cancelled',
messageId,
performance.now() - session.testGenerationStartTime,
getTelemetryReasonDesc(CodeWhispererConstants.unitTestGenerationCancelMessage)
getTelemetryReasonDesc(
`TestGenCancelled: ${CodeWhispererConstants.unitTestGenerationCancelMessage}`
),
'400',
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
'TestGenCancelled'
)

this.dispatcher.sendUpdatePromptProgress(
new UpdatePromptProgressMessage(tabID, cancellingProgressField)
)
Expand All @@ -293,9 +306,10 @@ export class Messenger {
false,
'Succeeded',
messageId,
performance.now() - session.testGenerationStartTime
performance.now() - session.testGenerationStartTime,
undefined,
'200'
)

this.dispatcher.sendUpdatePromptProgress(
new UpdatePromptProgressMessage(tabID, testGenCompletedField)
)
Expand Down
79 changes: 79 additions & 0 deletions packages/core/src/amazonqTest/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*!
* 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 statusCode: string,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we make this a number

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought of using number but in our Telemetry statusCode is declared as String.
aws-toolkit-common

If required I can use number here and can convert to string while emitting?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed this as part of this commit

public uiMessage: string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we don't need uiMessage. The ToolkitError.message field is explicitly documented as "potentially shown to the user". You can store the lower-level message on ToolkitError.details

this.details = info.details

This has far-reaching benefits. It allows all the existing architecture to treat these standard fields in a consistent way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like details has different type readonly details?: Record<string, unknown>

Copy link
Contributor

@justinmk3 justinmk3 Dec 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you look at existing code and figure out a way to make it work with details. It shouldn't matter that the type isn't exactly the same.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saw the usage of this but I think we can use the existing way as we use same structure for Reviewbird error handling too.
This change neither impacts other features nor functionality or telemetry.

) {
super(error, { code })
}
}
export class ProjectZipError extends TestGenError {
constructor(error: string) {
super(error, 'ProjectZipError', '400', defaultTestGenErrorMessage)
}
}
export class InvalidSourceZipError extends TestGenError {
constructor() {
super('Failed to create valid source zip', 'InvalidSourceZipError', '400', defaultTestGenErrorMessage)
}
}
export class CreateUploadUrlError extends TestGenError {
constructor(errorMessage: string, errorCode: string) {
super(errorMessage, 'CreateUploadUrlError', errorCode, technicalErrorCustomerFacingMessage)
}
}
export class UploadTestArtifactToS3Error extends TestGenError {
constructor(error: string, statusCode?: string) {
super(error, 'UploadTestArtifactToS3Error', statusCode ?? '400', technicalErrorCustomerFacingMessage)
}
}
export class CreateTestJobError extends TestGenError {
constructor(error: string, code: string) {
super(error, 'CreateTestJobError', code, technicalErrorCustomerFacingMessage)
}
}
export class TestGenTimedOutError extends TestGenError {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the plan here? Maintaining one-to-one errors with all the service-side errors is not maintainable. Can you find something more generalizable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TestGenTimedOutError is client side error if plugin did not get response for limited time we throw this error. This helps us understand the number of requests timed out from generic backend errors.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not asking about this error only, I'm referring to all of the errors in this file. It is not maintainable to create special errors for every service-side error.

constructor() {
super(
'Test generation failed. Amazon Q timed out.',
'TestGenTimedOutError',
'500',
technicalErrorCustomerFacingMessage
)
}
}
export class TestGenStoppedError extends TestGenError {
constructor() {
super('Unit test generation cancelled.', 'TestGenCancelled', '400', 'Unit test generation cancelled.')
}
}
export class TestGenFailedError extends TestGenError {
constructor(code: string, error?: string) {
super(
error ?? 'Test generation failed',
'TestGenFailedError',
code,
error ?? technicalErrorCustomerFacingMessage
)
}
}
export class ExportResultsArchiveError extends TestGenError {
constructor(error?: string, statusCode?: string) {
super(
error ?? 'Test generation failed',
'ExportResultsArchiveError',
statusCode ?? '400',
technicalErrorCustomerFacingMessage
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { ChildProcess, spawn } from 'child_process'
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 Expand Up @@ -121,7 +122,7 @@ export async function startTestGenerationProcess(
// TODO: Send status to test summary
if (jobStatus === TestGenerationJobStatus.FAILED) {
logger.verbose(`Test generation failed.`)
throw new TestGenFailedError()
throw new TestGenFailedError('500')
}
throwIfCancelled()
if (!shouldContinueRunning(tabID)) {
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 @@ -727,6 +727,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 @@ -867,7 +869,7 @@ export enum TestGenerationJobStatus {
COMPLETED = 'COMPLETED',
}

export enum ZipUseCase {
export enum FeatureUseCase {
TEST_GENERATION = 'TEST_GENERATION',
CODE_SCAN = 'CODE_SCAN',
}
Expand Down
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
Loading
Loading