Skip to content

Commit 20513ea

Browse files
committed
Fix
1 parent 917708a commit 20513ea

File tree

4 files changed

+113
-72
lines changed

4 files changed

+113
-72
lines changed

packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts

Lines changed: 2 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,9 @@ import { createSingleFileDialog } from '../../../shared/ui/common/openDialog'
1212
import {
1313
CodeIterationLimitError,
1414
ContentLengthError,
15-
ConversationIdNotFoundError,
1615
createUserFacingErrorMessage,
1716
denyListedErrors,
1817
FeatureDevServiceError,
19-
IllegalStateTransition,
2018
MonthlyConversationLimitError,
2119
NoChangeRequiredException,
2220
PrepareRepoFailedError,
@@ -46,7 +44,7 @@ import { getWorkspaceFoldersByPrefixes } from '../../../shared/utilities/workspa
4644
import { openDeletedDiff, openDiff } from '../../../amazonq/commons/diff'
4745
import { i18n } from '../../../shared/i18n-helper'
4846
import globals from '../../../shared/extensionGlobals'
49-
import { randomUUID } from '../../../shared'
47+
import { getStackTraceForError, randomUUID } from '../../../shared'
5048
import { FollowUpTypes } from '../../../amazonq/commons/types'
5149
import { Messenger } from '../../../amazonq/commons/connector/baseMessenger'
5250
import { BaseChatSessionStorage } from '../../../amazonq/commons/baseChatStorage'
@@ -522,7 +520,7 @@ export class FeatureDevController {
522520
await session.sendMetricDataTelemetry(
523521
MetricDataOperationName.EndCodeGeneration,
524522
result,
525-
'stack trace: ' + this.getStackTraceForError(err)
523+
'stack trace: ' + getStackTraceForError(err)
526524
)
527525
throw err
528526
} finally {
@@ -1019,56 +1017,4 @@ export class FeatureDevController {
10191017
})
10201018
}
10211019
}
1022-
1023-
// Should include error messages only for safe exceptions
1024-
// i.e. exceptions with deterministic error messages and do not include sensitive data
1025-
private getStackTraceForError(error: Error): string {
1026-
const recursionLimit = 3
1027-
const seenExceptions = new Set<Error>()
1028-
const lines: string[] = []
1029-
1030-
function printExceptionDetails(err: Error, depth: number, prefix: string = '') {
1031-
if (depth >= recursionLimit || seenExceptions.has(err)) {
1032-
return
1033-
}
1034-
seenExceptions.add(err)
1035-
1036-
if (
1037-
err instanceof FeatureDevServiceError ||
1038-
err instanceof ConversationIdNotFoundError ||
1039-
err instanceof TabIdNotFoundError ||
1040-
err instanceof WorkspaceFolderNotFoundError ||
1041-
err instanceof UserMessageNotFoundError ||
1042-
err instanceof SelectedFolderNotInWorkspaceFolderError ||
1043-
err instanceof PromptRefusalException ||
1044-
err instanceof NoChangeRequiredException ||
1045-
err instanceof PrepareRepoFailedError ||
1046-
err instanceof UploadCodeError ||
1047-
err instanceof UploadURLExpired ||
1048-
err instanceof IllegalStateTransition ||
1049-
err instanceof ContentLengthError ||
1050-
err instanceof ZipFileError ||
1051-
err instanceof CodeIterationLimitError
1052-
) {
1053-
lines.push(`${prefix}${err.constructor.name}: ${err.message}`)
1054-
} else {
1055-
lines.push(`${prefix}${err.constructor.name}`)
1056-
}
1057-
1058-
if (err.stack) {
1059-
const startStr = err.stack.substring('Error: '.length)
1060-
const callStack = startStr.substring(startStr.indexOf(err.message) + err.message.length + 1)
1061-
lines.push(`${prefix}${callStack}`)
1062-
}
1063-
1064-
const cause = (err as any).cause
1065-
if (cause instanceof Error) {
1066-
lines.push(`${prefix}\tCaused by: `)
1067-
printExceptionDetails(cause, depth + 1, `${prefix}\t`)
1068-
}
1069-
}
1070-
1071-
printExceptionDetails(error, 0)
1072-
return lines.join('\n')
1073-
}
10741020
}

packages/core/src/amazonqFeatureDev/errors.ts

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,112 +3,113 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { ToolkitError } from '../shared/errors'
6+
import { SafeMessageError, ToolkitError } from '../shared/errors'
77
import { featureName } from './constants'
88
import { uploadCodeError } from './userFacingText'
99
import { i18n } from '../shared/i18n-helper'
1010

11-
export class ConversationIdNotFoundError extends ToolkitError {
11+
export class ConversationIdNotFoundError extends ToolkitError implements SafeMessageError {
1212
constructor() {
1313
super(i18n('AWS.amazonq.featureDev.error.conversationIdNotFoundError'), {
1414
code: 'ConversationIdNotFound',
1515
})
1616
}
1717
}
1818

19-
export class TabIdNotFoundError extends ToolkitError {
19+
export class TabIdNotFoundError extends ToolkitError implements SafeMessageError {
2020
constructor() {
2121
super(i18n('AWS.amazonq.featureDev.error.tabIdNotFoundError'), {
2222
code: 'TabIdNotFound',
2323
})
2424
}
2525
}
2626

27-
export class WorkspaceFolderNotFoundError extends ToolkitError {
27+
export class WorkspaceFolderNotFoundError extends ToolkitError implements SafeMessageError {
2828
constructor() {
2929
super(i18n('AWS.amazonq.featureDev.error.workspaceFolderNotFoundError'), {
3030
code: 'WorkspaceFolderNotFound',
3131
})
3232
}
3333
}
3434

35-
export class UserMessageNotFoundError extends ToolkitError {
35+
export class UserMessageNotFoundError extends ToolkitError implements SafeMessageError {
3636
constructor() {
3737
super(i18n('AWS.amazonq.featureDev.error.userMessageNotFoundError'), {
3838
code: 'MessageNotFound',
3939
})
4040
}
4141
}
4242

43-
export class SelectedFolderNotInWorkspaceFolderError extends ToolkitError {
43+
export class SelectedFolderNotInWorkspaceFolderError extends ToolkitError implements SafeMessageError {
4444
constructor() {
4545
super(i18n('AWS.amazonq.featureDev.error.selectedFolderNotInWorkspaceFolderError'), {
4646
code: 'SelectedFolderNotInWorkspaceFolder',
4747
})
4848
}
4949
}
5050

51-
export class PromptRefusalException extends ToolkitError {
51+
export class PromptRefusalException extends ToolkitError implements SafeMessageError {
5252
constructor() {
5353
super(i18n('AWS.amazonq.featureDev.error.promptRefusalException'), {
5454
code: 'PromptRefusalException',
5555
})
5656
}
5757
}
5858

59-
export class NoChangeRequiredException extends ToolkitError {
59+
export class NoChangeRequiredException extends ToolkitError implements SafeMessageError {
6060
constructor() {
6161
super(i18n('AWS.amazonq.featureDev.error.noChangeRequiredException'), {
6262
code: 'NoChangeRequiredException',
6363
})
6464
}
6565
}
6666

67-
export class FeatureDevServiceError extends ToolkitError {
67+
// To prevent potential security issues, message passed in should be predictably safe for telemetry
68+
export class FeatureDevServiceError extends ToolkitError implements SafeMessageError {
6869
constructor(message: string, code: string) {
6970
super(message, { code })
7071
}
7172
}
7273

73-
export class PrepareRepoFailedError extends ToolkitError {
74+
export class PrepareRepoFailedError extends ToolkitError implements SafeMessageError {
7475
constructor() {
7576
super(i18n('AWS.amazonq.featureDev.error.prepareRepoFailedError'), {
7677
code: 'PrepareRepoFailed',
7778
})
7879
}
7980
}
8081

81-
export class UploadCodeError extends ToolkitError {
82+
export class UploadCodeError extends ToolkitError implements SafeMessageError {
8283
constructor(statusCode: string) {
8384
super(uploadCodeError, { code: `UploadCode-${statusCode}` })
8485
}
8586
}
8687

87-
export class UploadURLExpired extends ToolkitError {
88+
export class UploadURLExpired extends ToolkitError implements SafeMessageError {
8889
constructor() {
8990
super(i18n('AWS.amazonq.featureDev.error.uploadURLExpired'), { code: 'UploadURLExpired' })
9091
}
9192
}
9293

93-
export class IllegalStateTransition extends ToolkitError {
94+
export class IllegalStateTransition extends ToolkitError implements SafeMessageError {
9495
constructor() {
9596
super(i18n('AWS.amazonq.featureDev.error.illegalStateTransition'), { code: 'IllegalStateTransition' })
9697
}
9798
}
9899

99-
export class ContentLengthError extends ToolkitError {
100+
export class ContentLengthError extends ToolkitError implements SafeMessageError {
100101
constructor() {
101102
super(i18n('AWS.amazonq.featureDev.error.contentLengthError'), { code: ContentLengthError.name })
102103
}
103104
}
104105

105-
export class ZipFileError extends ToolkitError {
106+
export class ZipFileError extends ToolkitError implements SafeMessageError {
106107
constructor() {
107108
super(i18n('AWS.amazonq.featureDev.error.zipFileError'), { code: ZipFileError.name })
108109
}
109110
}
110111

111-
export class CodeIterationLimitError extends ToolkitError {
112+
export class CodeIterationLimitError extends ToolkitError implements SafeMessageError {
112113
constructor() {
113114
super(i18n('AWS.amazonq.featureDev.error.codeIterationLimitError'), { code: CodeIterationLimitError.name })
114115
}

packages/core/src/shared/errors.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,53 @@ export function getTelemetryResult(error: unknown | undefined): Result {
368368
return 'Failed'
369369
}
370370

371+
export class SafeMessageError extends Error {}
372+
373+
/**
374+
* This function constructs a string similar to error.printStackTrace() for telemetry,
375+
* but include error messages only for safe exceptions,
376+
* i.e. exceptions with deterministic error messages and do not include sensitive data.
377+
*
378+
* @param error The error to get stack trace string from
379+
*/
380+
export function getStackTraceForError(error: Error): string {
381+
const recursionLimit = 3
382+
const seenExceptions = new Set<Error>()
383+
const lines: string[] = []
384+
385+
function printExceptionDetails(err: Error, depth: number, prefix: string = '') {
386+
if (depth >= recursionLimit || seenExceptions.has(err)) {
387+
return
388+
}
389+
seenExceptions.add(err)
390+
391+
if (error instanceof SafeMessageError) {
392+
lines.push(`${prefix}${err.constructor.name}: ${err.message}`)
393+
} else {
394+
lines.push(`${prefix}${err.constructor.name}`)
395+
}
396+
397+
if (err.stack) {
398+
const startStr = err.stack.substring('Error: '.length)
399+
const callStack = startStr.substring(startStr.indexOf(err.message) + err.message.length + 1)
400+
for (const line of callStack.split('\n')) {
401+
const scrubbed = scrubNames(line, _username)
402+
lines.push(`${prefix}\t${scrubbed}`)
403+
}
404+
}
405+
406+
const cause = (err as any).cause
407+
if (cause instanceof Error) {
408+
lines.push(`${prefix}\tCaused by: `)
409+
printExceptionDetails(cause, depth + 1, `${prefix}\t`)
410+
}
411+
}
412+
413+
printExceptionDetails(error, 0)
414+
getLogger().info(lines.join('\n'))
415+
return lines.join('\n')
416+
}
417+
371418
/**
372419
* Removes potential PII from a string, for logging/telemetry.
373420
*

packages/core/src/test/shared/errors.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {
2020
tryRun,
2121
UnknownError,
2222
getErrorId,
23+
SafeMessageError,
24+
getStackTraceForError,
2325
} from '../../shared/errors'
2426
import { CancellationError } from '../../shared/utilities/timeoutUtils'
2527
import { UnauthorizedException } from '@aws-sdk/client-sso'
@@ -347,6 +349,51 @@ describe('Telemetry', function () {
347349
assert.strictEqual(getTelemetryReason(error2), 'ErrorCode')
348350
})
349351
})
352+
353+
describe('getStackTraceForError', () => {
354+
class SafeError extends ToolkitError implements SafeMessageError {}
355+
class UnsafeError extends ToolkitError {}
356+
357+
it('includes message for safe exceptions', () => {
358+
const safeError = new SafeError('Safe error message')
359+
const result = getStackTraceForError(safeError)
360+
361+
assert.ok(result.includes('Safe error message'))
362+
assert.ok(result.includes('SafeError: '))
363+
})
364+
365+
it('excludes message for unsafe exceptions', () => {
366+
const unsafeError = new UnsafeError('Sensitive information')
367+
const result = getStackTraceForError(unsafeError)
368+
369+
assert.ok(!result.includes('Sensitive information'))
370+
assert.ok(result.includes('UnsafeError'))
371+
})
372+
373+
it('respects recursion limit for nested exceptions', () => {
374+
const recursionLimit = 3
375+
const exceptions: ToolkitError[] = []
376+
377+
let currentError = new SafeError(`depth ${recursionLimit + 2}`)
378+
exceptions.push(currentError)
379+
380+
for (let i = recursionLimit + 1; i >= 0; i--) {
381+
currentError = new SafeError(`depth ${i}`, { cause: currentError })
382+
exceptions.push(currentError)
383+
}
384+
const stackTrace = getStackTraceForError(exceptions[exceptions.length - 1])
385+
386+
// Assert exceptions within limit are included
387+
for (let i = 0; i < recursionLimit; i++) {
388+
assert(stackTrace.includes(`depth ${i}`), `Stack trace should include error at depth ${i}`)
389+
}
390+
391+
// Assert exceptions beyond limit are not included
392+
for (let i = recursionLimit; i <= exceptions.length; i++) {
393+
assert(!stackTrace.includes(`depth ${i}`), `Stack trace should not include error at depth ${i}`)
394+
}
395+
})
396+
})
350397
})
351398

352399
describe('resolveErrorMessageToDisplay()', function () {

0 commit comments

Comments
 (0)