Skip to content

Commit 1d35262

Browse files
authored
Add auth login failure telemetry for cli (#3317)
* add auth login failure telemetry
1 parent 7233186 commit 1d35262

File tree

5 files changed

+136
-14
lines changed

5 files changed

+136
-14
lines changed

crates/chat-cli/src/auth/builder_id.rs

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,16 @@ pub enum TokenType {
273273
IamIdentityCenter,
274274
}
275275

276+
impl From<Option<&str>> for TokenType {
277+
fn from(start_url: Option<&str>) -> Self {
278+
match start_url {
279+
Some(url) if url == START_URL => TokenType::BuilderId,
280+
None => TokenType::BuilderId,
281+
Some(_) => TokenType::IamIdentityCenter,
282+
}
283+
}
284+
}
285+
276286
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
277287
pub struct BuilderIdToken {
278288
pub access_token: Secret,
@@ -302,7 +312,10 @@ impl BuilderIdToken {
302312
}
303313

304314
/// Load the token from the keychain, refresh the token if it is expired and return it
305-
pub async fn load(database: &Database) -> Result<Option<Self>, AuthError> {
315+
pub async fn load(
316+
database: &Database,
317+
telemetry: Option<&crate::telemetry::TelemetryThread>,
318+
) -> Result<Option<Self>, AuthError> {
306319
// Can't use #[cfg(test)] without breaking lints, and we don't want to require
307320
// authentication in order to run ChatSession tests. Hence, adding this here with cfg!(test)
308321
if cfg!(test) {
@@ -328,7 +341,7 @@ impl BuilderIdToken {
328341

329342
if token.is_expired() {
330343
trace!("token is expired, refreshing");
331-
token.refresh_token(&client, database, &region).await
344+
token.refresh_token(&client, database, &region, telemetry).await
332345
} else {
333346
trace!(?token, "found a valid token");
334347
Ok(Some(token))
@@ -357,6 +370,7 @@ impl BuilderIdToken {
357370
client: &Client,
358371
database: &Database,
359372
region: &Region,
373+
telemetry: Option<&crate::telemetry::TelemetryThread>,
360374
) -> Result<Option<Self>, AuthError> {
361375
let Some(refresh_token) = &self.refresh_token else {
362376
warn!("no refresh token was found");
@@ -416,6 +430,25 @@ impl BuilderIdToken {
416430
let display_err = DisplayErrorContext(&err);
417431
error!("Failed to refresh builder id access token: {}", display_err);
418432

433+
// Send telemetry for refresh failure
434+
if let Some(telemetry) = telemetry {
435+
let auth_method = match self.token_type() {
436+
TokenType::BuilderId => "BuilderId",
437+
TokenType::IamIdentityCenter => "IdentityCenter",
438+
};
439+
let oauth_flow = match self.oauth_flow {
440+
OAuthFlow::DeviceCode => "DeviceCode",
441+
OAuthFlow::Pkce => "PKCE",
442+
};
443+
let error_code = match &err {
444+
SdkError::ServiceError(service_err) => service_err.err().meta().code().map(|s| s.to_string()),
445+
_ => None,
446+
};
447+
telemetry
448+
.send_auth_failed(auth_method, oauth_flow, "TokenRefresh", error_code)
449+
.ok();
450+
}
451+
419452
// if the error is the client's fault, clear the token
420453
if let SdkError::ServiceError(service_err) = &err {
421454
if !service_err.err().is_slow_down_exception() {
@@ -471,11 +504,7 @@ impl BuilderIdToken {
471504
}
472505

473506
pub fn token_type(&self) -> TokenType {
474-
match &self.start_url {
475-
Some(url) if url == START_URL => TokenType::BuilderId,
476-
None => TokenType::BuilderId,
477-
Some(_) => TokenType::IamIdentityCenter,
478-
}
507+
TokenType::from(self.start_url.as_deref())
479508
}
480509

481510
/// Check if the token is for the internal amzn start URL (`https://amzn.awsapps.com/start`),
@@ -498,6 +527,7 @@ pub async fn poll_create_token(
498527
device_code: String,
499528
start_url: Option<String>,
500529
region: Option<String>,
530+
telemetry: &crate::telemetry::TelemetryThread,
501531
) -> PollCreateToken {
502532
let region = region.clone().map_or(OIDC_BUILDER_ID_REGION, Region::new);
503533
let client = client(region.clone());
@@ -538,6 +568,20 @@ pub async fn poll_create_token(
538568
},
539569
Err(err) => {
540570
error!(?err, "Failed to poll for builder id token");
571+
572+
// Send telemetry for device code failure
573+
let auth_method = match TokenType::from(start_url.as_deref()) {
574+
TokenType::BuilderId => "BuilderId",
575+
TokenType::IamIdentityCenter => "IdentityCenter",
576+
};
577+
let error_code = match &err {
578+
SdkError::ServiceError(service_err) => service_err.err().meta().code().map(|s| s.to_string()),
579+
_ => None,
580+
};
581+
telemetry
582+
.send_auth_failed(auth_method, "DeviceCode", "NewLogin", error_code)
583+
.ok();
584+
541585
PollCreateToken::Error(err.into())
542586
},
543587
}
@@ -550,7 +594,7 @@ pub async fn is_logged_in(database: &mut Database) -> bool {
550594
return true;
551595
}
552596

553-
match BuilderIdToken::load(database).await {
597+
match BuilderIdToken::load(database, None).await {
554598
Ok(Some(_)) => true,
555599
Ok(None) => {
556600
info!("not logged in - no valid token found");
@@ -585,7 +629,7 @@ pub async fn logout(database: &mut Database) -> Result<(), AuthError> {
585629
pub async fn get_start_url_and_region(database: &Database) -> (Option<String>, Option<String>) {
586630
// NOTE: Database provides direct methods to access the start_url and region, but they are not
587631
// guaranteed to be up to date in the chat session. Example: login is changed mid-chat session.
588-
let token = BuilderIdToken::load(database).await;
632+
let token = BuilderIdToken::load(database, None).await;
589633
match token {
590634
Ok(Some(t)) => (t.start_url, t.region),
591635
_ => (None, None),
@@ -603,7 +647,7 @@ impl ResolveIdentity for BearerResolver {
603647
) -> IdentityFuture<'a> {
604648
IdentityFuture::new_boxed(Box::pin(async {
605649
let database = Database::new().await?;
606-
match BuilderIdToken::load(&database).await? {
650+
match BuilderIdToken::load(&database, None).await? {
607651
Some(token) => Ok(Identity::new(
608652
Token::new(token.access_token.0.clone(), Some(token.expires_at.into())),
609653
Some(token.expires_at.into()),
@@ -618,7 +662,7 @@ pub async fn is_idc_user(database: &Database) -> Result<bool> {
618662
if cfg!(test) {
619663
return Ok(false);
620664
}
621-
if let Ok(Some(token)) = BuilderIdToken::load(database).await {
665+
if let Ok(Some(token)) = BuilderIdToken::load(database, None).await {
622666
Ok(token.token_type() == TokenType::IamIdentityCenter)
623667
} else {
624668
Err(eyre!("No auth token found - is the user signed in?"))

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,16 @@ impl LoginArgs {
143143
]);
144144
let ctrl_c_stream = ctrl_c();
145145
tokio::select! {
146-
res = registration.finish(&client, Some(&mut os.database)) => res?,
146+
res = registration.finish(&client, Some(&mut os.database)) => {
147+
if let Err(err) = res {
148+
let auth_method = match crate::auth::builder_id::TokenType::from(start_url.as_deref()) {
149+
crate::auth::builder_id::TokenType::BuilderId => "BuilderId",
150+
crate::auth::builder_id::TokenType::IamIdentityCenter => "IdentityCenter",
151+
};
152+
os.telemetry.send_auth_failed(auth_method, "PKCE", "NewLogin", None).ok();
153+
return Err(err.into());
154+
}
155+
},
147156
Ok(_) = ctrl_c_stream => {
148157
#[allow(clippy::exit)]
149158
exit(1);
@@ -194,7 +203,7 @@ pub struct WhoamiArgs {
194203

195204
impl WhoamiArgs {
196205
pub async fn execute(self, os: &mut Os) -> Result<ExitCode> {
197-
let builder_id = BuilderIdToken::load(&os.database).await;
206+
let builder_id = BuilderIdToken::load(&os.database, Some(&os.telemetry)).await;
198207

199208
match builder_id {
200209
Ok(Some(token)) => {
@@ -245,7 +254,7 @@ pub enum LicenseType {
245254
}
246255

247256
pub async fn profile(os: &mut Os) -> Result<ExitCode> {
248-
if let Ok(Some(token)) = BuilderIdToken::load(&os.database).await {
257+
if let Ok(Some(token)) = BuilderIdToken::load(&os.database, Some(&os.telemetry)).await {
249258
if matches!(token.token_type(), TokenType::BuilderId) {
250259
bail!("This command is only available for Pro users");
251260
}
@@ -314,6 +323,7 @@ async fn try_device_authorization(os: &mut Os, start_url: Option<String>, region
314323
device_auth.device_code.clone(),
315324
start_url.clone(),
316325
region.clone(),
326+
&os.telemetry,
317327
)
318328
.await
319329
{

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use crate::telemetry::definitions::metrics::{
2323
CodewhispererterminalAddChatMessage,
2424
CodewhispererterminalAgentConfigInit,
2525
CodewhispererterminalAgentContribution,
26+
CodewhispererterminalAuthFailed,
2627
CodewhispererterminalChatSlashCommandExecuted,
2728
CodewhispererterminalCliSubcommandExecuted,
2829
CodewhispererterminalMcpServerInit,
@@ -500,6 +501,24 @@ impl Event {
500501
}
501502
.into_metric_datum(),
502503
),
504+
EventType::AuthFailed {
505+
auth_method,
506+
oauth_flow,
507+
error_type,
508+
error_code,
509+
} => Some(
510+
CodewhispererterminalAuthFailed {
511+
create_time: self.created_time,
512+
value: None,
513+
credential_start_url: self.credential_start_url.map(Into::into),
514+
codewhispererterminal_in_cloudshell: None,
515+
codewhispererterminal_auth_method: Some(auth_method.into()),
516+
oauth_flow: Some(oauth_flow.into()),
517+
codewhispererterminal_error_type: Some(error_type.into()),
518+
codewhispererterminal_error_code: error_code.map(Into::into),
519+
}
520+
.into_metric_datum(),
521+
),
503522
EventType::DailyHeartbeat {} => Some(
504523
AmazonqcliDailyHeartbeat {
505524
create_time: self.created_time,
@@ -594,6 +613,12 @@ pub struct AgentConfigInitArgs {
594613
#[serde(tag = "type")]
595614
pub enum EventType {
596615
UserLoggedIn {},
616+
AuthFailed {
617+
auth_method: String,
618+
oauth_flow: String,
619+
error_type: String,
620+
error_code: Option<String>,
621+
},
597622
RefreshCredentials {
598623
request_id: String,
599624
result: TelemetryResult,

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,21 @@ impl TelemetryThread {
235235
Ok(self.tx.send(Event::new(EventType::UserLoggedIn {}))?)
236236
}
237237

238+
pub fn send_auth_failed(
239+
&self,
240+
auth_method: &str,
241+
oauth_flow: &str,
242+
error_type: &str,
243+
error_code: Option<String>,
244+
) -> Result<(), TelemetryError> {
245+
Ok(self.tx.send(Event::new(EventType::AuthFailed {
246+
auth_method: auth_method.to_string(),
247+
oauth_flow: oauth_flow.to_string(),
248+
error_type: error_type.to_string(),
249+
error_code,
250+
}))?)
251+
}
252+
238253
pub fn send_daily_heartbeat(&self) -> Result<(), TelemetryError> {
239254
Ok(self.tx.send(Event::new(EventType::DailyHeartbeat {}))?)
240255
}

crates/chat-cli/telemetry_definitions.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,21 @@
6565
"type": "string",
6666
"description": "The oauth authentication flow executed by the user, e.g. device code or PKCE"
6767
},
68+
{
69+
"name": "codewhispererterminal_authMethod",
70+
"type": "string",
71+
"description": "The authentication method used, e.g. BuilderId or IdentityCenter"
72+
},
73+
{
74+
"name": "codewhispererterminal_errorType",
75+
"type": "string",
76+
"description": "The type of authentication error, e.g. TokenRefresh or NewLogin"
77+
},
78+
{
79+
"name": "codewhispererterminal_errorCode",
80+
"type": "string",
81+
"description": "The specific error code from the authentication service"
82+
},
6883
{
6984
"name": "result",
7085
"type": "string",
@@ -369,6 +384,19 @@
369384
{ "type": "codewhispererterminal_inCloudshell" }
370385
]
371386
},
387+
{
388+
"name": "codewhispererterminal_authFailed",
389+
"description": "Emitted when authentication fails",
390+
"passive": false,
391+
"metadata": [
392+
{ "type": "credentialStartUrl" },
393+
{ "type": "codewhispererterminal_inCloudshell" },
394+
{ "type": "codewhispererterminal_authMethod" },
395+
{ "type": "oauthFlow" },
396+
{ "type": "codewhispererterminal_errorType" },
397+
{ "type": "codewhispererterminal_errorCode", "required": false }
398+
]
399+
},
372400
{
373401
"name": "codewhispererterminal_refreshCredentials",
374402
"description": "Emitted when users refresh their credentials",

0 commit comments

Comments
 (0)