Skip to content

Commit 7792907

Browse files
committed
add auth login failure telemetry
1 parent a275492 commit 7792907

File tree

5 files changed

+134
-14
lines changed

5 files changed

+134
-14
lines changed

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

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,10 @@ impl BuilderIdToken {
302302
}
303303

304304
/// 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> {
305+
pub async fn load(
306+
database: &Database,
307+
telemetry: Option<&crate::telemetry::TelemetryThread>,
308+
) -> Result<Option<Self>, AuthError> {
306309
// Can't use #[cfg(test)] without breaking lints, and we don't want to require
307310
// authentication in order to run ChatSession tests. Hence, adding this here with cfg!(test)
308311
if cfg!(test) {
@@ -328,7 +331,7 @@ impl BuilderIdToken {
328331

329332
if token.is_expired() {
330333
trace!("token is expired, refreshing");
331-
token.refresh_token(&client, database, &region).await
334+
token.refresh_token(&client, database, &region, telemetry).await
332335
} else {
333336
trace!(?token, "found a valid token");
334337
Ok(Some(token))
@@ -357,6 +360,7 @@ impl BuilderIdToken {
357360
client: &Client,
358361
database: &Database,
359362
region: &Region,
363+
telemetry: Option<&crate::telemetry::TelemetryThread>,
360364
) -> Result<Option<Self>, AuthError> {
361365
let Some(refresh_token) = &self.refresh_token else {
362366
warn!("no refresh token was found");
@@ -416,6 +420,25 @@ impl BuilderIdToken {
416420
let display_err = DisplayErrorContext(&err);
417421
error!("Failed to refresh builder id access token: {}", display_err);
418422

423+
// Send telemetry for refresh failure
424+
if let Some(telemetry) = telemetry {
425+
let auth_method = match self.token_type() {
426+
TokenType::BuilderId => "BuilderId",
427+
TokenType::IamIdentityCenter => "IdentityCenter",
428+
};
429+
let oauth_flow = match self.oauth_flow {
430+
OAuthFlow::DeviceCode => "DeviceCode",
431+
OAuthFlow::Pkce => "PKCE",
432+
};
433+
let error_code = match &err {
434+
SdkError::ServiceError(service_err) => service_err.err().meta().code().map(|s| s.to_string()),
435+
_ => None,
436+
};
437+
telemetry
438+
.send_auth_failed(auth_method, oauth_flow, "TokenRefresh", error_code)
439+
.ok();
440+
}
441+
419442
// if the error is the client's fault, clear the token
420443
if let SdkError::ServiceError(service_err) = &err {
421444
if !service_err.err().is_slow_down_exception() {
@@ -471,11 +494,7 @@ impl BuilderIdToken {
471494
}
472495

473496
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-
}
497+
token_type_from_start_url(self.start_url.as_deref())
479498
}
480499

481500
/// Check if the token is for the internal amzn start URL (`https://amzn.awsapps.com/start`),
@@ -486,6 +505,14 @@ impl BuilderIdToken {
486505
}
487506
}
488507

508+
pub fn token_type_from_start_url(start_url: Option<&str>) -> TokenType {
509+
match start_url {
510+
Some(url) if url == START_URL => TokenType::BuilderId,
511+
None => TokenType::BuilderId,
512+
Some(_) => TokenType::IamIdentityCenter,
513+
}
514+
}
515+
489516
pub enum PollCreateToken {
490517
Pending,
491518
Complete,
@@ -498,6 +525,7 @@ pub async fn poll_create_token(
498525
device_code: String,
499526
start_url: Option<String>,
500527
region: Option<String>,
528+
telemetry: &crate::telemetry::TelemetryThread,
501529
) -> PollCreateToken {
502530
let region = region.clone().map_or(OIDC_BUILDER_ID_REGION, Region::new);
503531
let client = client(region.clone());
@@ -538,6 +566,20 @@ pub async fn poll_create_token(
538566
},
539567
Err(err) => {
540568
error!(?err, "Failed to poll for builder id token");
569+
570+
// Send telemetry for device code failure
571+
let auth_method = match token_type_from_start_url(start_url.as_deref()) {
572+
TokenType::BuilderId => "BuilderId",
573+
TokenType::IamIdentityCenter => "IdentityCenter",
574+
};
575+
let error_code = match &err {
576+
SdkError::ServiceError(service_err) => service_err.err().meta().code().map(|s| s.to_string()),
577+
_ => None,
578+
};
579+
telemetry
580+
.send_auth_failed(auth_method, "DeviceCode", "NewLogin", error_code)
581+
.ok();
582+
541583
PollCreateToken::Error(err.into())
542584
},
543585
}
@@ -550,7 +592,7 @@ pub async fn is_logged_in(database: &mut Database) -> bool {
550592
return true;
551593
}
552594

553-
match BuilderIdToken::load(database).await {
595+
match BuilderIdToken::load(database, None).await {
554596
Ok(Some(_)) => true,
555597
Ok(None) => {
556598
info!("not logged in - no valid token found");
@@ -585,7 +627,7 @@ pub async fn logout(database: &mut Database) -> Result<(), AuthError> {
585627
pub async fn get_start_url_and_region(database: &Database) -> (Option<String>, Option<String>) {
586628
// NOTE: Database provides direct methods to access the start_url and region, but they are not
587629
// guaranteed to be up to date in the chat session. Example: login is changed mid-chat session.
588-
let token = BuilderIdToken::load(database).await;
630+
let token = BuilderIdToken::load(database, None).await;
589631
match token {
590632
Ok(Some(t)) => (t.start_url, t.region),
591633
_ => (None, None),
@@ -603,7 +645,7 @@ impl ResolveIdentity for BearerResolver {
603645
) -> IdentityFuture<'a> {
604646
IdentityFuture::new_boxed(Box::pin(async {
605647
let database = Database::new().await?;
606-
match BuilderIdToken::load(&database).await? {
648+
match BuilderIdToken::load(&database, None).await? {
607649
Some(token) => Ok(Identity::new(
608650
Token::new(token.access_token.0.clone(), Some(token.expires_at.into())),
609651
Some(token.expires_at.into()),
@@ -618,7 +660,7 @@ pub async fn is_idc_user(database: &Database) -> Result<bool> {
618660
if cfg!(test) {
619661
return Ok(false);
620662
}
621-
if let Ok(Some(token)) = BuilderIdToken::load(database).await {
663+
if let Ok(Some(token)) = BuilderIdToken::load(database, None).await {
622664
Ok(token.token_type() == TokenType::IamIdentityCenter)
623665
} else {
624666
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::token_type_from_start_url(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)