Skip to content

Commit aa3ba5e

Browse files
authored
fix(amazonq): Update exception handling for feature dev to improve observability (#4953)
**Problem:** Feature dev instrumentation 1) records all failures as `FeatureDevException`, losing information about the specific exception type, and 2) lacks annotation of `reasonDesc` and the operation related to the error. (By operation, although we do capture `amazonq_codeGenerationInvoke`, we're missing the subprocess name such as `StartCodeGeneration`.) As a result, we do not have insight into feature dev failures in JetBrains. **Solution:** 1) Update exception modeling pattern for feature dev functionality to derive subclasses of `FeatureDevException`, providing a meaningful class name when inspected as error `reason`. 2) On `FeatureDevException`, accept an `operation` and `desc` to provide further annotation (merged when logged as `reasonDesc`). This change will have conflicts with #4938 and #4949. I'll rebase on top of those and reconcile any drift in a subsequent revision.
1 parent 96331c8 commit aa3ba5e

File tree

9 files changed

+152
-87
lines changed

9 files changed

+152
-87
lines changed

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevConstants.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,16 @@ enum class ModifySourceFolderErrorReason(
2525

2626
override fun toString(): String = reasonText
2727
}
28+
29+
enum class FeatureDevOperation(private val operationName: String) {
30+
StartTaskAssistCodeGeneration("StartTaskAssistCodeGenerator"),
31+
CreateConversation("CreateConversation"),
32+
CreateUploadUrl("CreateUploadUrl"),
33+
GenerateCode("GenerateCode"),
34+
GetTaskAssistCodeGeneration("GetTaskAssistCodeGenerator"),
35+
ExportTaskAssistArchiveResult("ExportTaskAssistArchiveResult"),
36+
UploadToS3("UploadToS3"),
37+
;
38+
39+
override fun toString(): String = operationName
40+
}

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevExceptions.kt

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,69 @@ package software.aws.toolkits.jetbrains.services.amazonqFeatureDev
66
import software.aws.toolkits.jetbrains.services.amazonq.RepoSizeError
77
import software.aws.toolkits.resources.message
88

9-
open class FeatureDevException(override val message: String?, override val cause: Throwable? = null) : RuntimeException()
9+
/**
10+
* FeatureDevException models failures from feature dev operations.
11+
*
12+
* - Each failure is annotated based on className, operation, and a short desc. Use the `reason()` and `reasonDesc()` members for instrumentation.
13+
* - To throw an exception without modeling, throw FeatureDevException directly.
14+
*/
15+
open class FeatureDevException(override val message: String?, val operation: String, val desc: String?, override val cause: Throwable? = null) :
16+
RuntimeException() {
17+
fun reason(): String = this.javaClass.simpleName
1018

11-
class ContentLengthError(override val message: String, override val cause: Throwable?) : RepoSizeError, RuntimeException()
19+
fun reasonDesc(): String =
20+
when (desc) {
21+
desc -> "$operation | Description: $desc"
22+
else -> operation
23+
}
24+
}
1225

13-
class ZipFileError(override val message: String, override val cause: Throwable?) : RuntimeException()
26+
class NoChangeRequiredException(operation: String, desc: String?, cause: Throwable? = null) :
27+
FeatureDevException(message("amazonqFeatureDev.exception.no_change_required_exception"), operation, desc, cause)
1428

15-
class CodeIterationLimitError(override val message: String, override val cause: Throwable?) : RuntimeException()
29+
class EmptyPatchException(operation: String, desc: String?, cause: Throwable? = null) :
30+
FeatureDevException(message("amazonqFeatureDev.exception.guardrails"), operation, desc, cause)
1631

17-
class MonthlyConversationLimitError(override val message: String, override val cause: Throwable?) : RuntimeException()
32+
class ContentLengthException(
33+
override val message: String = message("amazonqFeatureDev.content_length.error_text"),
34+
operation: String,
35+
desc: String?,
36+
cause: Throwable? = null,
37+
) :
38+
RepoSizeError, FeatureDevException(message, operation, desc, cause)
1839

19-
class UploadURLExpired(
20-
override val message: String = message(
21-
"amazonqFeatureDev.exception.upload_url_expiry"
22-
),
23-
override val cause: Throwable? = null,
24-
) : FeatureDevException(message, cause)
40+
class ZipFileCorruptedException(operation: String, desc: String?, cause: Throwable? = null) :
41+
FeatureDevException("The zip file is corrupted", operation, desc, cause)
2542

26-
internal fun featureDevServiceError(message: String?): Nothing =
27-
throw FeatureDevException(message)
43+
class UploadURLExpired(operation: String, desc: String?, cause: Throwable? = null) :
44+
FeatureDevException(message("amazonqFeatureDev.exception.upload_url_expiry"), operation, desc, cause)
2845

29-
internal fun codeGenerationFailedError(): Nothing =
30-
throw FeatureDevException(message("amazonqFeatureDev.code_generation.failed_generation"))
46+
class CodeIterationLimitException(operation: String, desc: String?, cause: Throwable? = null) :
47+
FeatureDevException(message("amazonqFeatureDev.code_generation.iteration_limit.error_text"), operation, desc, cause)
3148

32-
internal fun uploadCodeError(): Nothing =
33-
throw FeatureDevException(message("amazonqFeatureDev.exception.upload_code"))
49+
class MonthlyConversationLimitError(message: String, operation: String, desc: String?, cause: Throwable? = null) :
50+
FeatureDevException(message, operation, desc, cause)
3451

35-
internal fun conversationIdNotFound(): Nothing =
36-
throw FeatureDevException(message("amazonqFeatureDev.exception.conversation_not_found"))
52+
class GuardrailsException(operation: String, desc: String?, cause: Throwable? = null) :
53+
FeatureDevException(message("amazonqFeatureDev.exception.guardrails"), operation, desc, cause)
3754

38-
internal fun apiError(message: String?, cause: Throwable?): Nothing =
39-
throw FeatureDevException(message, cause)
55+
class PromptRefusalException(operation: String, desc: String?, cause: Throwable? = null) :
56+
FeatureDevException(message("amazonqFeatureDev.exception.prompt_refusal"), operation, desc, cause)
4057

41-
internal fun exportParseError(): Nothing =
42-
throw FeatureDevException(message("amazonqFeatureDev.exception.export_parsing_error"))
58+
class ThrottlingException(operation: String, desc: String?, cause: Throwable? = null) :
59+
FeatureDevException(message("amazonqFeatureDev.exception.throttling"), operation, desc, cause)
60+
61+
class ExportParseException(operation: String, desc: String?, cause: Throwable? = null) :
62+
FeatureDevException(message("amazonqFeatureDev.exception.export_parsing_error"), operation, desc, cause)
63+
64+
class CodeGenerationException(operation: String, desc: String?, cause: Throwable? = null) :
65+
FeatureDevException(message("amazonqFeatureDev.code_generation.failed_generation"), operation, desc, cause)
66+
67+
class UploadCodeException(operation: String, desc: String?, cause: Throwable? = null) :
68+
FeatureDevException(message("amazonqFeatureDev.exception.upload_code"), operation, desc, cause)
69+
70+
class ConversationIdNotFoundException(operation: String, desc: String?, cause: Throwable? = null) :
71+
FeatureDevException(message("amazonqFeatureDev.exception.conversation_not_found"), operation, desc, cause)
4372

4473
val denyListedErrors = arrayOf("Deserialization error", "Inaccessible host", "UnknownHost")
4574
fun createUserFacingErrorMessage(message: String?): String? =

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@ import software.aws.toolkits.jetbrains.services.amazonq.RepoSizeError
2929
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
3030
import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController
3131
import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindowFactory
32-
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.CodeIterationLimitError
32+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.CodeIterationLimitException
3333
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.DEFAULT_RETRY_LIMIT
3434
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FEATURE_NAME
3535
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FeatureDevException
3636
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.InboundAppMessagesHandler
3737
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ModifySourceFolderErrorReason
3838
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.MonthlyConversationLimitError
3939
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.UploadURLExpired
40-
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ZipFileError
40+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ZipFileCorruptedException
4141
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.createUserFacingErrorMessage
4242
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.denyListedErrors
4343
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.FeatureDevMessageType
@@ -433,7 +433,7 @@ class FeatureDevController(
433433
),
434434
)
435435
}
436-
is ZipFileError -> {
436+
is ZipFileCorruptedException -> {
437437
messenger.sendError(
438438
tabId = tabId,
439439
errMessage = err.message,
@@ -451,15 +451,7 @@ class FeatureDevController(
451451
messageType = FeatureDevMessageType.Answer,
452452
canBeVoted = true
453453
)
454-
is FeatureDevException -> {
455-
messenger.sendError(
456-
tabId = tabId,
457-
errMessage = err.message,
458-
retries = retriesRemaining(session),
459-
conversationId = session?.conversationIdUnsafe
460-
)
461-
}
462-
is CodeIterationLimitError -> {
454+
is CodeIterationLimitException -> {
463455
messenger.sendError(
464456
tabId = tabId,
465457
errMessage = err.message,
@@ -479,24 +471,36 @@ class FeatureDevController(
479471
)
480472
}
481473
else -> {
482-
var msg = createUserFacingErrorMessage("$FEATURE_NAME request failed: ${err.message ?: err.cause?.message}")
483-
val isDenyListedError = denyListedErrors.any { msg?.contains(it) ?: false }
484-
val defaultMessage: String = when (session?.sessionState?.phase) {
485-
SessionStatePhase.CODEGEN -> {
486-
if (isDenyListedError || retriesRemaining(session) > 0) {
487-
message("amazonqFeatureDev.code_generation.error_message")
488-
} else {
489-
message("amazonqFeatureDev.code_generation.no_retries.error_message")
474+
when (err) {
475+
is FeatureDevException -> {
476+
messenger.sendError(
477+
tabId = tabId,
478+
errMessage = err.message,
479+
retries = retriesRemaining(session),
480+
conversationId = session?.conversationIdUnsafe
481+
)
482+
}
483+
else -> {
484+
val msg = createUserFacingErrorMessage("$FEATURE_NAME request failed: ${err.message ?: err.cause?.message}")
485+
val isDenyListedError = denyListedErrors.any { msg?.contains(it) ?: false }
486+
val defaultMessage: String = when (session?.sessionState?.phase) {
487+
SessionStatePhase.CODEGEN -> {
488+
if (isDenyListedError || retriesRemaining(session) > 0) {
489+
message("amazonqFeatureDev.code_generation.error_message")
490+
} else {
491+
message("amazonqFeatureDev.code_generation.no_retries.error_message")
492+
}
493+
}
494+
else -> message("amazonqFeatureDev.error_text")
490495
}
496+
messenger.sendError(
497+
tabId = tabId,
498+
errMessage = defaultMessage,
499+
retries = retriesRemaining(session),
500+
conversationId = session?.conversationIdUnsafe
501+
)
491502
}
492-
else -> message("amazonqFeatureDev.error_text")
493503
}
494-
messenger.sendError(
495-
tabId = tabId,
496-
errMessage = defaultMessage,
497-
retries = retriesRemaining(session),
498-
conversationId = session?.conversationIdUnsafe
499-
)
500504
}
501505
}
502506
}

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/CodeGenerationState.kt

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@ import software.amazon.awssdk.services.codewhispererruntime.model.CodeGeneration
88
import software.aws.toolkits.core.utils.getLogger
99
import software.aws.toolkits.core.utils.warn
1010
import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher
11+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.CodeGenerationException
12+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.EmptyPatchException
1113
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FEATURE_NAME
12-
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.codeGenerationFailedError
13-
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.featureDevServiceError
14+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FeatureDevException
15+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FeatureDevOperation
16+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.GuardrailsException
17+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.PromptRefusalException
18+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ThrottlingException
1419
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.sendAnswerPart
1520
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.sendUpdatePlaceholder
1621
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
@@ -37,6 +42,7 @@ class CodeGenerationState(
3742
val startTime = System.currentTimeMillis()
3843
var result: Result = Result.Succeeded
3944
var failureReason: String? = null
45+
var failureReasonDesc: String? = null
4046
var codeGenerationWorkflowStatus: CodeGenerationWorkflowStatus = CodeGenerationWorkflowStatus.COMPLETE
4147
var numberOfReferencesGenerated: Int? = null
4248
var numberOfFilesGenerated: Int? = null
@@ -86,6 +92,10 @@ class CodeGenerationState(
8692
logger.warn(e) { "$FEATURE_NAME: Code generation failed: ${e.message}" }
8793
result = Result.Failed
8894
failureReason = e.javaClass.simpleName
95+
if (e is FeatureDevException) {
96+
failureReason = e.reason()
97+
failureReasonDesc = e.reasonDesc()
98+
}
8999
codeGenerationWorkflowStatus = CodeGenerationWorkflowStatus.FAILED
90100

91101
throw e
@@ -100,6 +110,7 @@ class CodeGenerationState(
100110
amazonqRepositorySize = repositorySize,
101111
result = result,
102112
reason = failureReason,
113+
reasonDesc = failureReasonDesc,
103114
duration = (System.currentTimeMillis() - startTime).toDouble(),
104115
credentialStartUrl = getStartUrl(config.featureDevService.project)
105116
)
@@ -149,20 +160,20 @@ private suspend fun CodeGenerationState.generateCode(codeGenerationId: String, m
149160
codeGenerationResultState.codeGenerationStatusDetail()?.contains(
150161
"Guardrails"
151162
),
152-
-> featureDevServiceError(message("amazonqFeatureDev.exception.guardrails"))
163+
-> throw GuardrailsException(operation = FeatureDevOperation.GenerateCode.toString(), desc = "Failed guardrails")
153164
codeGenerationResultState.codeGenerationStatusDetail()?.contains(
154165
"PromptRefusal"
155166
),
156-
-> featureDevServiceError(message("amazonqFeatureDev.exception.prompt_refusal"))
167+
-> throw PromptRefusalException(operation = FeatureDevOperation.GenerateCode.toString(), desc = "Prompt refusal")
157168
codeGenerationResultState.codeGenerationStatusDetail()?.contains(
158169
"EmptyPatch"
159170
),
160-
-> featureDevServiceError(message("amazonqFeatureDev.exception.guardrails"))
171+
-> throw EmptyPatchException(operation = FeatureDevOperation.GenerateCode.toString(), desc = "Empty patch")
161172
codeGenerationResultState.codeGenerationStatusDetail()?.contains(
162173
"Throttling"
163174
),
164-
-> featureDevServiceError(message("amazonqFeatureDev.exception.throttling"))
165-
else -> codeGenerationFailedError()
175+
-> throw ThrottlingException(operation = FeatureDevOperation.GenerateCode.toString(), desc = "Request throttled")
176+
else -> throw CodeGenerationException(operation = FeatureDevOperation.GenerateCode.toString(), desc = null)
166177
}
167178
}
168179
else -> error("Unknown status: ${codeGenerationResultState.codeGenerationStatus().status()}")

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import com.intellij.openapi.vfs.VfsUtil
99
import software.aws.toolkits.jetbrains.services.amazonq.FeatureDevSessionContext
1010
import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher
1111
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.CODE_GENERATION_RETRY_LIMIT
12+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ConversationIdNotFoundException
1213
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FEATURE_NAME
1314
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.MAX_PROJECT_SIZE_BYTES
1415
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.clients.FeatureDevClient
15-
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.conversationIdNotFound
1616
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.sendAsyncEventProgress
1717
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FeatureDevService
1818
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.resolveAndCreateOrUpdateFile
@@ -138,7 +138,7 @@ class Session(val tabID: String, val project: Project) {
138138
val conversationId: String
139139
get() {
140140
if (_conversationId == null) {
141-
conversationIdNotFound()
141+
throw ConversationIdNotFoundException(operation = "Session", desc = "Conversation ID not found")
142142
} else {
143143
return _conversationId as String
144144
}

0 commit comments

Comments
 (0)