diff --git a/crates/chat-cli/src/auth/builder_id.rs b/crates/chat-cli/src/auth/builder_id.rs
index 3fa1496e4f..55eec36458 100644
--- a/crates/chat-cli/src/auth/builder_id.rs
+++ b/crates/chat-cli/src/auth/builder_id.rs
@@ -302,7 +302,10 @@ impl BuilderIdToken {
}
/// Load the token from the keychain, refresh the token if it is expired and return it
- pub async fn load(database: &Database) -> Result, AuthError> {
+ pub async fn load(
+ database: &Database,
+ telemetry: Option<&crate::telemetry::TelemetryThread>,
+ ) -> Result , AuthError> {
// Can't use #[cfg(test)] without breaking lints, and we don't want to require
// authentication in order to run ChatSession tests. Hence, adding this here with cfg!(test)
if cfg!(test) {
@@ -328,7 +331,7 @@ impl BuilderIdToken {
if token.is_expired() {
trace!("token is expired, refreshing");
- token.refresh_token(&client, database, ®ion).await
+ token.refresh_token(&client, database, ®ion, telemetry).await
} else {
trace!(?token, "found a valid token");
Ok(Some(token))
@@ -357,6 +360,7 @@ impl BuilderIdToken {
client: &Client,
database: &Database,
region: &Region,
+ telemetry: Option<&crate::telemetry::TelemetryThread>,
) -> Result , AuthError> {
let Some(refresh_token) = &self.refresh_token else {
warn!("no refresh token was found");
@@ -416,6 +420,25 @@ impl BuilderIdToken {
let display_err = DisplayErrorContext(&err);
error!("Failed to refresh builder id access token: {}", display_err);
+ // Send telemetry for refresh failure
+ if let Some(telemetry) = telemetry {
+ let auth_method = match self.token_type() {
+ TokenType::BuilderId => "BuilderId",
+ TokenType::IamIdentityCenter => "IdentityCenter",
+ };
+ let oauth_flow = match self.oauth_flow {
+ OAuthFlow::DeviceCode => "DeviceCode",
+ OAuthFlow::Pkce => "PKCE",
+ };
+ let error_code = match &err {
+ SdkError::ServiceError(service_err) => service_err.err().meta().code().map(|s| s.to_string()),
+ _ => None,
+ };
+ telemetry
+ .send_auth_failed(auth_method, oauth_flow, "TokenRefresh", error_code)
+ .ok();
+ }
+
// if the error is the client's fault, clear the token
if let SdkError::ServiceError(service_err) = &err {
if !service_err.err().is_slow_down_exception() {
@@ -471,11 +494,7 @@ impl BuilderIdToken {
}
pub fn token_type(&self) -> TokenType {
- match &self.start_url {
- Some(url) if url == START_URL => TokenType::BuilderId,
- None => TokenType::BuilderId,
- Some(_) => TokenType::IamIdentityCenter,
- }
+ token_type_from_start_url(self.start_url.as_deref())
}
/// Check if the token is for the internal amzn start URL (`https://amzn.awsapps.com/start`),
@@ -486,6 +505,14 @@ impl BuilderIdToken {
}
}
+pub fn token_type_from_start_url(start_url: Option<&str>) -> TokenType {
+ match start_url {
+ Some(url) if url == START_URL => TokenType::BuilderId,
+ None => TokenType::BuilderId,
+ Some(_) => TokenType::IamIdentityCenter,
+ }
+}
+
pub enum PollCreateToken {
Pending,
Complete,
@@ -498,6 +525,7 @@ pub async fn poll_create_token(
device_code: String,
start_url: Option,
region: Option,
+ telemetry: &crate::telemetry::TelemetryThread,
) -> PollCreateToken {
let region = region.clone().map_or(OIDC_BUILDER_ID_REGION, Region::new);
let client = client(region.clone());
@@ -538,6 +566,20 @@ pub async fn poll_create_token(
},
Err(err) => {
error!(?err, "Failed to poll for builder id token");
+
+ // Send telemetry for device code failure
+ let auth_method = match token_type_from_start_url(start_url.as_deref()) {
+ TokenType::BuilderId => "BuilderId",
+ TokenType::IamIdentityCenter => "IdentityCenter",
+ };
+ let error_code = match &err {
+ SdkError::ServiceError(service_err) => service_err.err().meta().code().map(|s| s.to_string()),
+ _ => None,
+ };
+ telemetry
+ .send_auth_failed(auth_method, "DeviceCode", "NewLogin", error_code)
+ .ok();
+
PollCreateToken::Error(err.into())
},
}
@@ -550,7 +592,7 @@ pub async fn is_logged_in(database: &mut Database) -> bool {
return true;
}
- match BuilderIdToken::load(database).await {
+ match BuilderIdToken::load(database, None).await {
Ok(Some(_)) => true,
Ok(None) => {
info!("not logged in - no valid token found");
@@ -585,7 +627,7 @@ pub async fn logout(database: &mut Database) -> Result<(), AuthError> {
pub async fn get_start_url_and_region(database: &Database) -> (Option, Option) {
// NOTE: Database provides direct methods to access the start_url and region, but they are not
// guaranteed to be up to date in the chat session. Example: login is changed mid-chat session.
- let token = BuilderIdToken::load(database).await;
+ let token = BuilderIdToken::load(database, None).await;
match token {
Ok(Some(t)) => (t.start_url, t.region),
_ => (None, None),
@@ -603,7 +645,7 @@ impl ResolveIdentity for BearerResolver {
) -> IdentityFuture<'a> {
IdentityFuture::new_boxed(Box::pin(async {
let database = Database::new().await?;
- match BuilderIdToken::load(&database).await? {
+ match BuilderIdToken::load(&database, None).await? {
Some(token) => Ok(Identity::new(
Token::new(token.access_token.0.clone(), Some(token.expires_at.into())),
Some(token.expires_at.into()),
@@ -618,7 +660,7 @@ pub async fn is_idc_user(database: &Database) -> Result {
if cfg!(test) {
return Ok(false);
}
- if let Ok(Some(token)) = BuilderIdToken::load(database).await {
+ if let Ok(Some(token)) = BuilderIdToken::load(database, None).await {
Ok(token.token_type() == TokenType::IamIdentityCenter)
} else {
Err(eyre!("No auth token found - is the user signed in?"))
diff --git a/crates/chat-cli/src/cli/user.rs b/crates/chat-cli/src/cli/user.rs
index 240da92caf..6ec77795eb 100644
--- a/crates/chat-cli/src/cli/user.rs
+++ b/crates/chat-cli/src/cli/user.rs
@@ -143,7 +143,16 @@ impl LoginArgs {
]);
let ctrl_c_stream = ctrl_c();
tokio::select! {
- res = registration.finish(&client, Some(&mut os.database)) => res?,
+ res = registration.finish(&client, Some(&mut os.database)) => {
+ if let Err(err) = res {
+ let auth_method = match crate::auth::builder_id::token_type_from_start_url(start_url.as_deref()) {
+ crate::auth::builder_id::TokenType::BuilderId => "BuilderId",
+ crate::auth::builder_id::TokenType::IamIdentityCenter => "IdentityCenter",
+ };
+ os.telemetry.send_auth_failed(auth_method, "PKCE", "NewLogin", None).ok();
+ return Err(err.into());
+ }
+ },
Ok(_) = ctrl_c_stream => {
#[allow(clippy::exit)]
exit(1);
@@ -194,7 +203,7 @@ pub struct WhoamiArgs {
impl WhoamiArgs {
pub async fn execute(self, os: &mut Os) -> Result {
- let builder_id = BuilderIdToken::load(&os.database).await;
+ let builder_id = BuilderIdToken::load(&os.database, Some(&os.telemetry)).await;
match builder_id {
Ok(Some(token)) => {
@@ -245,7 +254,7 @@ pub enum LicenseType {
}
pub async fn profile(os: &mut Os) -> Result {
- if let Ok(Some(token)) = BuilderIdToken::load(&os.database).await {
+ if let Ok(Some(token)) = BuilderIdToken::load(&os.database, Some(&os.telemetry)).await {
if matches!(token.token_type(), TokenType::BuilderId) {
bail!("This command is only available for Pro users");
}
@@ -314,6 +323,7 @@ async fn try_device_authorization(os: &mut Os, start_url: Option, region
device_auth.device_code.clone(),
start_url.clone(),
region.clone(),
+ &os.telemetry,
)
.await
{
diff --git a/crates/chat-cli/src/telemetry/core.rs b/crates/chat-cli/src/telemetry/core.rs
index 58091ae8ff..b33a3cf027 100644
--- a/crates/chat-cli/src/telemetry/core.rs
+++ b/crates/chat-cli/src/telemetry/core.rs
@@ -23,6 +23,7 @@ use crate::telemetry::definitions::metrics::{
CodewhispererterminalAddChatMessage,
CodewhispererterminalAgentConfigInit,
CodewhispererterminalAgentContribution,
+ CodewhispererterminalAuthFailed,
CodewhispererterminalChatSlashCommandExecuted,
CodewhispererterminalCliSubcommandExecuted,
CodewhispererterminalMcpServerInit,
@@ -500,6 +501,24 @@ impl Event {
}
.into_metric_datum(),
),
+ EventType::AuthFailed {
+ auth_method,
+ oauth_flow,
+ error_type,
+ error_code,
+ } => Some(
+ CodewhispererterminalAuthFailed {
+ create_time: self.created_time,
+ value: None,
+ credential_start_url: self.credential_start_url.map(Into::into),
+ codewhispererterminal_in_cloudshell: None,
+ codewhispererterminal_auth_method: Some(auth_method.into()),
+ oauth_flow: Some(oauth_flow.into()),
+ codewhispererterminal_error_type: Some(error_type.into()),
+ codewhispererterminal_error_code: error_code.map(Into::into),
+ }
+ .into_metric_datum(),
+ ),
EventType::DailyHeartbeat {} => Some(
AmazonqcliDailyHeartbeat {
create_time: self.created_time,
@@ -594,6 +613,12 @@ pub struct AgentConfigInitArgs {
#[serde(tag = "type")]
pub enum EventType {
UserLoggedIn {},
+ AuthFailed {
+ auth_method: String,
+ oauth_flow: String,
+ error_type: String,
+ error_code: Option,
+ },
RefreshCredentials {
request_id: String,
result: TelemetryResult,
diff --git a/crates/chat-cli/src/telemetry/mod.rs b/crates/chat-cli/src/telemetry/mod.rs
index 35f657fc85..3c6ef9af43 100644
--- a/crates/chat-cli/src/telemetry/mod.rs
+++ b/crates/chat-cli/src/telemetry/mod.rs
@@ -235,6 +235,21 @@ impl TelemetryThread {
Ok(self.tx.send(Event::new(EventType::UserLoggedIn {}))?)
}
+ pub fn send_auth_failed(
+ &self,
+ auth_method: &str,
+ oauth_flow: &str,
+ error_type: &str,
+ error_code: Option,
+ ) -> Result<(), TelemetryError> {
+ Ok(self.tx.send(Event::new(EventType::AuthFailed {
+ auth_method: auth_method.to_string(),
+ oauth_flow: oauth_flow.to_string(),
+ error_type: error_type.to_string(),
+ error_code,
+ }))?)
+ }
+
pub fn send_daily_heartbeat(&self) -> Result<(), TelemetryError> {
Ok(self.tx.send(Event::new(EventType::DailyHeartbeat {}))?)
}
diff --git a/crates/chat-cli/telemetry_definitions.json b/crates/chat-cli/telemetry_definitions.json
index 5dac4ec712..b2bc7044bf 100644
--- a/crates/chat-cli/telemetry_definitions.json
+++ b/crates/chat-cli/telemetry_definitions.json
@@ -65,6 +65,21 @@
"type": "string",
"description": "The oauth authentication flow executed by the user, e.g. device code or PKCE"
},
+ {
+ "name": "codewhispererterminal_authMethod",
+ "type": "string",
+ "description": "The authentication method used, e.g. BuilderId or IdentityCenter"
+ },
+ {
+ "name": "codewhispererterminal_errorType",
+ "type": "string",
+ "description": "The type of authentication error, e.g. TokenRefresh or NewLogin"
+ },
+ {
+ "name": "codewhispererterminal_errorCode",
+ "type": "string",
+ "description": "The specific error code from the authentication service"
+ },
{
"name": "result",
"type": "string",
@@ -369,6 +384,19 @@
{ "type": "codewhispererterminal_inCloudshell" }
]
},
+ {
+ "name": "codewhispererterminal_authFailed",
+ "description": "Emitted when authentication fails",
+ "passive": false,
+ "metadata": [
+ { "type": "credentialStartUrl" },
+ { "type": "codewhispererterminal_inCloudshell" },
+ { "type": "codewhispererterminal_authMethod" },
+ { "type": "oauthFlow" },
+ { "type": "codewhispererterminal_errorType" },
+ { "type": "codewhispererterminal_errorCode", "required": false }
+ ]
+ },
{
"name": "codewhispererterminal_refreshCredentials",
"description": "Emitted when users refresh their credentials",