-
Notifications
You must be signed in to change notification settings - Fork 471
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
base: master
Are you sure you want to change the base?
Changes from 15 commits
645971b
0afde36
d5994da
a3a3894
b9239d4
8a18b92
fc60bf0
0cf0945
cea2049
218b43d
b232e2e
39a9a64
990b577
5428d8d
456fa23
f5e043e
9a04527
9dbc32d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,7 @@ import { | |
TelemetryHelper, | ||
TestGenerationBuildStep, | ||
testGenState, | ||
tooManyRequestErrorMessage, | ||
unitTestGenerationCancelMessage, | ||
} from '../../../codewhisperer' | ||
import { | ||
|
@@ -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'), | ||
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 | ||
|
@@ -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, | ||
|
@@ -734,7 +752,6 @@ export class TestController { | |
) | ||
|
||
await this.endSession(message, FollowUpTypes.SkipBuildAndFinish) | ||
await this.sessionCleanUp() | ||
return | ||
|
||
if (session.listOfTestGenerationJobId.length === 1) { | ||
|
@@ -842,6 +859,7 @@ export class TestController { | |
session.startTestGenerationRequestId, | ||
session.latencyOfTestGeneration, | ||
undefined, | ||
'200', | ||
session.isCodeBlockSelected, | ||
session.artifactsUploadDuration, | ||
session.srcPayloadSize, | ||
|
@@ -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') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
this.messenger.sendChatInputEnabled(data.tabID, true) | ||
return | ||
} | ||
|
@@ -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 | ||
|
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, | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we make this a number There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. If required I can use number here and can convert to string while emitting? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed this as part of this commit |
||||
public uiMessage: string | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we don't need uiMessage. The
This has far-reaching benefits. It allows all the existing architecture to treat these standard fields in a consistent way. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like details has different type There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||
) { | ||||
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 { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||
) | ||||
} | ||||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.