Emit usage-limit prompt analytics from TUI

This commit is contained in:
James Chu
2026-04-27 18:34:26 -04:00
parent 3e95c582fa
commit 847e9e0691
7 changed files with 138 additions and 1 deletions
+4
View File
@@ -112,9 +112,13 @@ use codex_app_server_protocol::ThreadLoadedListParams;
use codex_app_server_protocol::ThreadMemoryMode;
use codex_app_server_protocol::ThreadRollbackResponse;
use codex_app_server_protocol::ThreadStartSource;
use codex_app_server_protocol::TrackUsageLimitBannerAction;
use codex_app_server_protocol::TrackUsageLimitBannerParams;
use codex_app_server_protocol::TrackUsageLimitBannerResponse;
use codex_app_server_protocol::Turn;
use codex_app_server_protocol::TurnError as AppServerTurnError;
use codex_app_server_protocol::TurnStatus;
use codex_app_server_protocol::UsageLimitBannerType;
use codex_config::ConfigLayerStackOrdering;
use codex_config::types::ApprovalsReviewer;
use codex_config::types::ModelAvailabilityNuxConfig;
@@ -59,6 +59,20 @@ impl App {
});
}
pub(super) fn track_usage_limit_banner(
&mut self,
app_server: &AppServerSession,
action: TrackUsageLimitBannerAction,
banner_type: UsageLimitBannerType,
) {
let request_handle = app_server.request_handle();
tokio::spawn(async move {
if let Err(err) = track_usage_limit_banner(request_handle, action, banner_type).await {
tracing::warn!("failed to track usage limit banner event: {err:#}");
}
});
}
/// Starts the initial skills refresh without delaying the first interactive frame.
///
/// Startup only needs skill metadata to populate skill mentions and the skills UI; the prompt can be
@@ -451,6 +465,26 @@ pub(super) async fn send_add_credits_nudge_email(
Ok(response.status)
}
pub(super) async fn track_usage_limit_banner(
request_handle: AppServerRequestHandle,
action: TrackUsageLimitBannerAction,
banner_type: UsageLimitBannerType,
) -> Result<()> {
let request_id = RequestId::String(format!("usage-limit-banner-{}", Uuid::new_v4()));
let _: TrackUsageLimitBannerResponse = request_handle
.request_typed(ClientRequest::TrackUsageLimitBanner {
request_id,
params: TrackUsageLimitBannerParams {
action,
banner_type,
},
})
.await
.wrap_err("account/usageLimitBanner/track failed in TUI")?;
Ok(())
}
pub(super) async fn fetch_skills_list(
request_handle: AppServerRequestHandle,
cwd: PathBuf,
+6
View File
@@ -564,6 +564,12 @@ impl App {
self.send_add_credits_nudge_email(app_server, credit_type);
}
}
AppEvent::TrackUsageLimitBanner {
action,
banner_type,
} => {
self.track_usage_limit_banner(app_server, action, banner_type);
}
AppEvent::AddCreditsNudgeEmailFinished { result } => {
self.chat_widget
.finish_add_credits_nudge_email_request(result);
+8
View File
@@ -22,6 +22,8 @@ use codex_app_server_protocol::PluginReadResponse;
use codex_app_server_protocol::PluginUninstallResponse;
use codex_app_server_protocol::SkillsListResponse;
use codex_app_server_protocol::ThreadGoalStatus;
use codex_app_server_protocol::TrackUsageLimitBannerAction;
use codex_app_server_protocol::UsageLimitBannerType;
use codex_file_search::FileMatch;
use codex_protocol::ThreadId;
use codex_protocol::openai_models::ModelPreset;
@@ -234,6 +236,12 @@ pub(crate) enum AppEvent {
credit_type: AddCreditsNudgeCreditType,
},
/// Track an analytics event for the workspace-owner nudge prompt.
TrackUsageLimitBanner {
action: TrackUsageLimitBannerAction,
banner_type: UsageLimitBannerType,
},
/// Result of notifying the workspace owner.
AddCreditsNudgeEmailFinished {
result: Result<AddCreditsNudgeEmailStatus, String>,
+18
View File
@@ -106,10 +106,12 @@ use codex_app_server_protocol::ThreadGoalStatus as AppThreadGoalStatus;
use codex_app_server_protocol::ThreadItem;
use codex_app_server_protocol::ThreadTokenUsage;
use codex_app_server_protocol::ToolRequestUserInputParams;
use codex_app_server_protocol::TrackUsageLimitBannerAction;
use codex_app_server_protocol::Turn;
use codex_app_server_protocol::TurnCompletedNotification;
use codex_app_server_protocol::TurnPlanStepStatus;
use codex_app_server_protocol::TurnStatus;
use codex_app_server_protocol::UsageLimitBannerType;
use codex_chatgpt::connectors;
use codex_config::ConfigLayerStackOrdering;
use codex_config::types::ApprovalsReviewer;
@@ -8581,7 +8583,19 @@ impl ChatWidget {
"Request a limit increase from your owner to continue using codex. Request increase?",
),
};
let banner_type = match credit_type {
AddCreditsNudgeCreditType::Credits => {
UsageLimitBannerType::WorkspaceMemberCreditsDepleted
}
AddCreditsNudgeCreditType::UsageLimit => {
UsageLimitBannerType::WorkspaceMemberUsageLimitReached
}
};
let send_actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
tx.send(AppEvent::TrackUsageLimitBanner {
action: TrackUsageLimitBannerAction::CtaClicked,
banner_type,
});
tx.send(AppEvent::SendAddCreditsNudgeEmail { credit_type });
})];
let items = vec![
@@ -8609,6 +8623,10 @@ impl ChatWidget {
initial_selected_idx: Some(1),
..Default::default()
});
self.app_event_tx.send(AppEvent::TrackUsageLimitBanner {
action: TrackUsageLimitBannerAction::Shown,
banner_type,
});
}
pub(crate) fn start_add_credits_nudge_email_request(
+2
View File
@@ -88,11 +88,13 @@ pub(super) use codex_app_server_protocol::ServerNotification;
pub(super) use codex_app_server_protocol::SkillSummary;
pub(super) use codex_app_server_protocol::ThreadClosedNotification;
pub(super) use codex_app_server_protocol::ThreadItem as AppServerThreadItem;
pub(super) use codex_app_server_protocol::TrackUsageLimitBannerAction;
pub(super) use codex_app_server_protocol::Turn as AppServerTurn;
pub(super) use codex_app_server_protocol::TurnCompletedNotification;
pub(super) use codex_app_server_protocol::TurnError as AppServerTurnError;
pub(super) use codex_app_server_protocol::TurnStartedNotification;
pub(super) use codex_app_server_protocol::TurnStatus as AppServerTurnStatus;
pub(super) use codex_app_server_protocol::UsageLimitBannerType;
pub(super) use codex_app_server_protocol::UserInput as AppServerUserInput;
pub(super) use codex_app_server_protocol::WarningNotification;
pub(super) use codex_config::AppRequirementToml;
@@ -603,8 +603,24 @@ async fn workspace_member_credits_depleted_prompts_and_sends_credits() {
);
let popup = render_bottom_popup(&chat, /*width*/ 90);
assert_chatwidget_snapshot!("workspace_member_credits_depleted_prompt", popup);
let event = next_track_usage_limit_banner_event(&mut rx);
assert_eq!(
event,
(
TrackUsageLimitBannerAction::Shown,
UsageLimitBannerType::WorkspaceMemberCreditsDepleted
)
);
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
let event = next_track_usage_limit_banner_event(&mut rx);
assert_eq!(
event,
(
TrackUsageLimitBannerAction::CtaClicked,
UsageLimitBannerType::WorkspaceMemberCreditsDepleted
)
);
let event = next_send_add_credits_nudge_email_event(&mut rx);
assert_eq!(event, AddCreditsNudgeCreditType::Credits);
}
@@ -623,8 +639,24 @@ async fn workspace_member_usage_limit_prompts_and_sends_usage_limit() {
);
let popup = render_bottom_popup(&chat, /*width*/ 100);
assert_chatwidget_snapshot!("workspace_member_usage_limit_prompt", popup);
let event = next_track_usage_limit_banner_event(&mut rx);
assert_eq!(
event,
(
TrackUsageLimitBannerAction::Shown,
UsageLimitBannerType::WorkspaceMemberUsageLimitReached
)
);
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
let event = next_track_usage_limit_banner_event(&mut rx);
assert_eq!(
event,
(
TrackUsageLimitBannerAction::CtaClicked,
UsageLimitBannerType::WorkspaceMemberUsageLimitReached
)
);
let event = next_send_add_credits_nudge_email_event(&mut rx);
assert_eq!(event, AddCreditsNudgeCreditType::UsageLimit);
}
@@ -776,6 +808,14 @@ async fn workspace_owner_nudge_default_no_dismisses_without_sending() {
RateLimitErrorKind::Generic,
"Usage limit reached.".to_string(),
);
let event = next_track_usage_limit_banner_event(&mut rx);
assert_eq!(
event,
(
TrackUsageLimitBannerAction::Shown,
UsageLimitBannerType::WorkspaceMemberCreditsDepleted
)
);
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert_no_owner_nudge_or_rate_limit_refresh(&mut rx);
@@ -793,6 +833,14 @@ async fn workspace_owner_nudge_reappears_after_dismissing_no() {
RateLimitErrorKind::UsageLimit,
"Usage limit reached.".to_string(),
);
let event = next_track_usage_limit_banner_event(&mut rx);
assert_eq!(
event,
(
TrackUsageLimitBannerAction::Shown,
UsageLimitBannerType::WorkspaceMemberUsageLimitReached
)
);
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert_no_owner_nudge_or_rate_limit_refresh(&mut rx);
@@ -892,6 +940,21 @@ fn next_send_add_credits_nudge_email_event(
panic!("expected SendAddCreditsNudgeEmail app event");
}
fn next_track_usage_limit_banner_event(
rx: &mut tokio::sync::mpsc::UnboundedReceiver<AppEvent>,
) -> (TrackUsageLimitBannerAction, UsageLimitBannerType) {
while let Ok(event) = rx.try_recv() {
if let AppEvent::TrackUsageLimitBanner {
action,
banner_type,
} = event
{
return (action, banner_type);
}
}
panic!("expected TrackUsageLimitBanner app event");
}
fn assert_no_owner_nudge_or_rate_limit_refresh(
rx: &mut tokio::sync::mpsc::UnboundedReceiver<AppEvent>,
) {
@@ -899,7 +962,9 @@ fn assert_no_owner_nudge_or_rate_limit_refresh(
assert!(
!matches!(
event,
AppEvent::SendAddCreditsNudgeEmail { .. } | AppEvent::RefreshRateLimits { .. }
AppEvent::SendAddCreditsNudgeEmail { .. }
| AppEvent::RefreshRateLimits { .. }
| AppEvent::TrackUsageLimitBanner { .. }
),
"unexpected event: {event:?}"
);