Skip to content

Commit 11be917

Browse files
authored
feat(telemetry): stable reason codes, reasonDesc field (#137)
1 parent 098deeb commit 11be917

File tree

7 files changed

+126
-29
lines changed

7 files changed

+126
-29
lines changed

crates/chat-cli/src/api_client/error.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ use amzn_consolas_client::operation::generate_recommendations::GenerateRecommend
88
use amzn_consolas_client::operation::list_customizations::ListCustomizationsError;
99
use amzn_qdeveloper_streaming_client::operation::send_message::SendMessageError as QDeveloperSendMessageError;
1010
use amzn_qdeveloper_streaming_client::types::error::ChatResponseStreamError as QDeveloperChatResponseStreamError;
11+
use aws_sdk_ssooidc::error::ProvideErrorMetadata;
1112
use aws_smithy_runtime_api::client::orchestrator::HttpResponse;
1213
pub use aws_smithy_runtime_api::client::result::SdkError;
1314
use aws_smithy_types::event_stream::RawMessage;
1415
use thiserror::Error;
1516

1617
use crate::auth::AuthError;
1718
use crate::aws_common::SdkErrorDisplay;
19+
use crate::telemetry::ReasonCode;
1820

1921
#[derive(Debug, Error)]
2022
pub enum ApiClientError {
@@ -68,6 +70,36 @@ pub enum ApiClientError {
6870
AuthError(#[from] AuthError),
6971
}
7072

73+
impl ReasonCode for ApiClientError {
74+
fn reason_code(&self) -> String {
75+
match self {
76+
ApiClientError::GenerateCompletions(e) => sdk_error_code(e),
77+
ApiClientError::GenerateRecommendations(e) => sdk_error_code(e),
78+
ApiClientError::ListAvailableCustomizations(e) => sdk_error_code(e),
79+
ApiClientError::ListAvailableServices(e) => sdk_error_code(e),
80+
ApiClientError::CodewhispererGenerateAssistantResponse(e) => sdk_error_code(e),
81+
ApiClientError::QDeveloperSendMessage(e) => sdk_error_code(e),
82+
ApiClientError::CodewhispererChatResponseStream(e) => sdk_error_code(e),
83+
ApiClientError::QDeveloperChatResponseStream(e) => sdk_error_code(e),
84+
ApiClientError::ListAvailableProfilesError(e) => sdk_error_code(e),
85+
ApiClientError::SendTelemetryEvent(e) => sdk_error_code(e),
86+
ApiClientError::QuotaBreach(_) => "QuotaBreachError".to_string(),
87+
ApiClientError::ContextWindowOverflow => "ContextWindowOverflow".to_string(),
88+
ApiClientError::SmithyBuild(_) => "SmithyBuildError".to_string(),
89+
ApiClientError::AuthError(_) => "AuthError".to_string(),
90+
}
91+
}
92+
}
93+
94+
fn sdk_error_code<T, R>(e: &SdkError<T, R>) -> String
95+
where
96+
T: ProvideErrorMetadata,
97+
{
98+
e.as_service_error()
99+
.and_then(|se| se.meta().code().map(str::to_string))
100+
.unwrap_or_else(|| e.to_string())
101+
}
102+
71103
#[cfg(test)]
72104
mod tests {
73105
use std::error::Error as _;

crates/chat-cli/src/cli/chat/mod.rs

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ use std::collections::{
2222
HashSet,
2323
VecDeque,
2424
};
25-
use std::error::Error;
2625
use std::io::{
2726
IsTerminal,
2827
Read,
@@ -66,7 +65,6 @@ use crossterm::{
6665
terminal,
6766
};
6867
use eyre::{
69-
Chain,
7068
ErrReport,
7169
Result,
7270
bail,
@@ -158,8 +156,10 @@ use crate::mcp_client::{
158156
use crate::platform::Context;
159157
use crate::telemetry::core::ToolUseEventBuilder;
160158
use crate::telemetry::{
159+
ReasonCode,
161160
TelemetryResult,
162161
TelemetryThread,
162+
get_error_reason,
163163
};
164164

165165
#[derive(Debug, Clone, PartialEq, Eq, Default, Args)]
@@ -494,6 +494,21 @@ pub enum ChatError {
494494
GetPromptError(#[from] GetPromptError),
495495
}
496496

497+
impl ReasonCode for ChatError {
498+
fn reason_code(&self) -> String {
499+
match self {
500+
ChatError::Client(e) => e.reason_code(),
501+
ChatError::ResponseStream(e) => e.reason_code(),
502+
ChatError::Std(_) => "StdIoError".to_string(),
503+
ChatError::Readline(_) => "ReadlineError".to_string(),
504+
ChatError::Custom(_) => "GenericError".to_string(),
505+
ChatError::Interrupted { .. } => "Interrupted".to_string(),
506+
ChatError::NonInteractiveToolApproval => "NonInteractiveToolApprovalError".to_string(),
507+
ChatError::GetPromptError(_) => "GetPromptError".to_string(),
508+
}
509+
}
510+
}
511+
497512
pub struct ChatContext {
498513
ctx: Arc<Context>,
499514
/// The [Write] destination for printing conversation text.
@@ -869,7 +884,7 @@ impl ChatContext {
869884
ChatState::HandleResponseStream(response) => tokio::select! {
870885
res = self.handle_response(database, telemetry, response) => res,
871886
Ok(_) = ctrl_c_stream => {
872-
self.send_chat_telemetry(database, telemetry, None, TelemetryResult::Cancelled, None).await;
887+
self.send_chat_telemetry(database, telemetry, None, TelemetryResult::Cancelled, None, None).await;
873888

874889
Err(ChatError::Interrupted { tool_uses: None })
875890
}
@@ -894,7 +909,8 @@ impl ChatContext {
894909
match result {
895910
Ok(state) => Ok(state),
896911
Err(e) => {
897-
self.send_error_telemetry(database, telemetry, get_error_string(&e))
912+
let (reason, reason_desc) = get_error_reason(&e);
913+
self.send_error_telemetry(database, telemetry, reason, Some(reason_desc))
898914
.await;
899915

900916
macro_rules! print_err {
@@ -1089,12 +1105,14 @@ impl ChatContext {
10891105
let response = match response {
10901106
Ok(res) => res,
10911107
Err(e) => {
1108+
let (reason, reason_desc) = get_error_reason(&e);
10921109
self.send_chat_telemetry(
10931110
database,
10941111
telemetry,
10951112
None,
10961113
TelemetryResult::Failed,
1097-
Some(get_error_string(&e)),
1114+
Some(reason),
1115+
Some(reason_desc),
10981116
)
10991117
.await;
11001118
match e {
@@ -1137,12 +1155,14 @@ impl ChatContext {
11371155
if let Some(request_id) = &err.request_id {
11381156
self.failed_request_ids.push(request_id.clone());
11391157
};
1158+
let (reason, reason_desc) = get_error_reason(&err);
11401159
self.send_chat_telemetry(
11411160
database,
11421161
telemetry,
11431162
err.request_id.clone(),
11441163
TelemetryResult::Failed,
1145-
Some(get_error_string(&err)),
1164+
Some(reason),
1165+
Some(reason_desc),
11461166
)
11471167
.await;
11481168
return Err(err.into());
@@ -1160,7 +1180,8 @@ impl ChatContext {
11601180
cursor::Show
11611181
)?;
11621182
}
1163-
self.send_chat_telemetry(database, telemetry, request_id, TelemetryResult::Succeeded, None)
1183+
1184+
self.send_chat_telemetry(database, telemetry, request_id, TelemetryResult::Succeeded, None, None)
11641185
.await;
11651186

11661187
self.conversation_state.replace_history_with_summary(summary.clone());
@@ -3266,12 +3287,14 @@ impl ChatContext {
32663287
self.failed_request_ids.push(request_id.clone());
32673288
};
32683289

3290+
let (reason, reason_desc) = get_error_reason(&recv_error);
32693291
self.send_chat_telemetry(
32703292
database,
32713293
telemetry,
32723294
recv_error.request_id.clone(),
32733295
TelemetryResult::Failed,
3274-
Some(get_error_string(&recv_error)),
3296+
Some(reason),
3297+
Some(reason_desc),
32753298
)
32763299
.await;
32773300

@@ -3383,7 +3406,7 @@ impl ChatContext {
33833406
}
33843407

33853408
if ended {
3386-
self.send_chat_telemetry(database, telemetry, request_id, TelemetryResult::Succeeded, None)
3409+
self.send_chat_telemetry(database, telemetry, request_id, TelemetryResult::Succeeded, None, None)
33873410
.await;
33883411

33893412
if self.interactive
@@ -3680,6 +3703,7 @@ impl ChatContext {
36803703
request_id: Option<String>,
36813704
result: TelemetryResult,
36823705
reason: Option<String>,
3706+
reason_desc: Option<String>,
36833707
) {
36843708
telemetry
36853709
.send_chat_added_message(
@@ -3690,19 +3714,27 @@ impl ChatContext {
36903714
self.conversation_state.context_message_length(),
36913715
result,
36923716
reason,
3717+
reason_desc,
36933718
)
36943719
.await
36953720
.ok();
36963721
}
36973722

3698-
async fn send_error_telemetry(&self, database: &Database, telemetry: &TelemetryThread, reason: String) {
3723+
async fn send_error_telemetry(
3724+
&self,
3725+
database: &Database,
3726+
telemetry: &TelemetryThread,
3727+
reason: String,
3728+
reason_desc: Option<String>,
3729+
) {
36993730
telemetry
37003731
.send_response_error(
37013732
database,
37023733
self.conversation_state.conversation_id().to_owned(),
37033734
self.conversation_state.context_message_length(),
37043735
TelemetryResult::Failed,
37053736
Some(reason),
3737+
reason_desc,
37063738
)
37073739
.await
37083740
.ok();
@@ -3805,22 +3837,6 @@ fn create_stream(model_responses: serde_json::Value) -> StreamingClient {
38053837
StreamingClient::mock(mock)
38063838
}
38073839

3808-
/// Returns surface error + root cause as a string. If there is only one error
3809-
/// in the chain, return that as a string.
3810-
fn get_error_string(error: &(dyn Error + 'static)) -> String {
3811-
let err_chain = Chain::new(error);
3812-
3813-
if err_chain.len() > 1 {
3814-
format!(
3815-
"'{}' caused by: {}",
3816-
error,
3817-
err_chain.last().map_or("UNKNOWN".to_string(), |e| e.to_string())
3818-
)
3819-
} else {
3820-
error.to_string()
3821-
}
3822-
}
3823-
38243840
#[cfg(test)]
38253841
mod tests {
38263842
use super::*;

crates/chat-cli/src/cli/chat/parser.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use super::message::{
2121
};
2222
use crate::api_client::clients::SendMessageOutput;
2323
use crate::api_client::model::ChatResponseStream;
24+
use crate::telemetry::ReasonCode;
2425

2526
#[derive(Debug, Error)]
2627
pub struct RecvError {
@@ -30,6 +31,12 @@ pub struct RecvError {
3031
pub source: RecvErrorKind,
3132
}
3233

34+
impl ReasonCode for RecvError {
35+
fn reason_code(&self) -> String {
36+
"RecvError".to_string()
37+
}
38+
}
39+
3340
impl std::fmt::Display for RecvError {
3441
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3542
write!(f, "Failed to receive the next message: ")?;

crates/chat-cli/src/telemetry/core.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ impl Event {
130130
request_id,
131131
result,
132132
reason,
133+
reason_desc,
133134
..
134135
} => Some(
135136
CodewhispererterminalAddChatMessage {
@@ -144,6 +145,7 @@ impl Event {
144145
codewhispererterminal_context_file_length: context_file_length.map(|l| l as i64).map(Into::into),
145146
result: result.to_string().into(),
146147
reason: reason.map(Into::into),
148+
reason_desc: reason_desc.map(Into::into),
147149
}
148150
.into_metric_datum(),
149151
),
@@ -242,6 +244,7 @@ impl Event {
242244
context_file_length,
243245
result,
244246
reason,
247+
reason_desc,
245248
} => Some(
246249
AmazonqMessageResponseError {
247250
create_time: self.created_time,
@@ -252,6 +255,7 @@ impl Event {
252255
sso_region: self.sso_region.map(Into::into),
253256
result: Some(result.to_string().into()),
254257
reason: reason.map(Into::into),
258+
reason_desc: reason_desc.map(Into::into),
255259
}
256260
.into_metric_datum(),
257261
),
@@ -286,6 +290,7 @@ pub enum EventType {
286290
context_file_length: Option<usize>,
287291
result: TelemetryResult,
288292
reason: Option<String>,
293+
reason_desc: Option<String>,
289294
},
290295
ToolUseSuggested {
291296
conversation_id: String,
@@ -322,6 +327,7 @@ pub enum EventType {
322327
MessageResponseError {
323328
result: TelemetryResult,
324329
reason: Option<String>,
330+
reason_desc: Option<String>,
325331
conversation_id: String,
326332
context_file_length: Option<usize>,
327333
},

crates/chat-cli/src/telemetry/definitions.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ mod tests {
2929
codewhispererterminal_utterance_id: Some("message_id".to_owned().into()),
3030
result: crate::telemetry::definitions::types::Result::new("Succeeded".to_string()),
3131
reason: None,
32+
reason_desc: None,
3233
});
3334

3435
let s = serde_json::to_string_pretty(&metric_datum_init).unwrap();

crates/chat-cli/src/telemetry/mod.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ impl TelemetryThread {
195195
context_file_length: Option<usize>,
196196
result: TelemetryResult,
197197
reason: Option<String>,
198+
reason_desc: Option<String>,
198199
) -> Result<(), TelemetryError> {
199200
let mut event = Event::new(EventType::ChatAddedMessage {
200201
conversation_id,
@@ -203,6 +204,7 @@ impl TelemetryThread {
203204
context_file_length,
204205
result,
205206
reason,
207+
reason_desc,
206208
});
207209
set_start_url_and_region(database, &mut event).await;
208210

@@ -278,10 +280,12 @@ impl TelemetryThread {
278280
context_file_length: Option<usize>,
279281
result: TelemetryResult,
280282
reason: Option<String>,
283+
reason_desc: Option<String>,
281284
) -> Result<(), TelemetryError> {
282285
let mut event = Event::new(EventType::MessageResponseError {
283286
result,
284287
reason,
288+
reason_desc,
285289
conversation_id,
286290
context_file_length,
287291
});
@@ -476,6 +480,29 @@ impl TelemetryClient {
476480
}
477481
}
478482

483+
pub trait ReasonCode: std::error::Error {
484+
fn reason_code(&self) -> String;
485+
}
486+
487+
/// Returns a generic error reason + reason description pair.
488+
pub fn get_error_reason<E>(error: &E) -> (String, String)
489+
where
490+
E: ReasonCode + 'static,
491+
{
492+
let err_chain = eyre::Chain::new(error);
493+
let reason_desc = if err_chain.len() > 1 {
494+
format!(
495+
"'{}' caused by: {}",
496+
error,
497+
err_chain.last().map_or("UNKNOWN".to_string(), |e| e.to_string())
498+
)
499+
} else {
500+
error.to_string()
501+
};
502+
503+
(error.reason_code(), reason_desc)
504+
}
505+
479506
#[cfg(test)]
480507
mod test {
481508
use uuid::uuid;
@@ -537,6 +564,7 @@ mod test {
537564
Some(123),
538565
TelemetryResult::Succeeded,
539566
None,
567+
None,
540568
)
541569
.await
542570
.ok();

0 commit comments

Comments
 (0)