agent_ui: Add additional telemetry for thread errors (#47971)
This PR extends error telemetry coverage in the agent panel, building on #46836 which added telemetry for thread errors. `"Agent Panel Error Shown"` (extended) Now also fires for: - **Load errors** - when an agent fails to start - **Configuration errors** - when LLM provider setup is incomplete `"Agent Token Limit Warning"` (new event) Fires when a thread approaches or exceeds its token/context limit. This is tracked separately from errors because it's an informational warning, not a failure. ## Implementation Notes - All telemetry fires **once per error occurrence**, not on every render - Load error telemetry fires in `handle_load_error()` when the error is first received - Token limit telemetry uses a flag to track state transitions (Normal → Warning → Exceeded) - Configuration error telemetry tracks the last emitted error kind to avoid duplicates Release Notes: - N/A *or* Added/Fixed/Improved ... --------- Co-authored-by: Michael Benfield <mbenfield@zed.dev> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9c3526f2c4
commit
955581bdfd
4 files changed
+157
-42
No files matched your search
@@ -932,7 +932,7 @@ impl TokenUsage {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum TokenUsageRatio {
|
||||
Normal,
|
||||
Warning,
|
||||
|
||||
@@ -797,12 +797,13 @@ impl AcpServerView {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some(load_err) = err.downcast_ref::<LoadError>() {
|
||||
self.server_state = ServerState::LoadError(load_err.clone());
|
||||
let load_error = if let Some(load_err) = err.downcast_ref::<LoadError>() {
|
||||
load_err.clone()
|
||||
} else {
|
||||
self.server_state =
|
||||
ServerState::LoadError(LoadError::Other(format!("{:#}", err).into()))
|
||||
}
|
||||
LoadError::Other(format!("{:#}", err).into())
|
||||
};
|
||||
self.emit_load_error_telemetry(&load_error);
|
||||
self.server_state = ServerState::LoadError(load_error);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -1069,6 +1070,7 @@ impl AcpServerView {
|
||||
}
|
||||
AcpThreadEvent::TokenUsageUpdated => {
|
||||
self.update_turn_tokens(cx);
|
||||
self.emit_token_limit_telemetry_if_needed(thread, cx);
|
||||
}
|
||||
AcpThreadEvent::AvailableCommandsUpdated(available_commands) => {
|
||||
let mut available_commands = available_commands.clone();
|
||||
@@ -1628,6 +1630,77 @@ impl AcpServerView {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn emit_token_limit_telemetry_if_needed(
|
||||
&mut self,
|
||||
thread: &Entity<AcpThread>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(active_thread) = self.as_active_thread() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let (ratio, agent_telemetry_id, session_id) = {
|
||||
let thread_data = thread.read(cx);
|
||||
let Some(token_usage) = thread_data.token_usage() else {
|
||||
return;
|
||||
};
|
||||
(
|
||||
token_usage.ratio(),
|
||||
thread_data.connection().telemetry_id(),
|
||||
thread_data.session_id().clone(),
|
||||
)
|
||||
};
|
||||
|
||||
let kind = match ratio {
|
||||
acp_thread::TokenUsageRatio::Normal => {
|
||||
active_thread.update(cx, |active, _cx| {
|
||||
active.last_token_limit_telemetry = None;
|
||||
});
|
||||
return;
|
||||
}
|
||||
acp_thread::TokenUsageRatio::Warning => "warning",
|
||||
acp_thread::TokenUsageRatio::Exceeded => "exceeded",
|
||||
};
|
||||
|
||||
let should_skip = active_thread
|
||||
.read(cx)
|
||||
.last_token_limit_telemetry
|
||||
.as_ref()
|
||||
.is_some_and(|last| *last >= ratio);
|
||||
if should_skip {
|
||||
return;
|
||||
}
|
||||
|
||||
active_thread.update(cx, |active, _cx| {
|
||||
active.last_token_limit_telemetry = Some(ratio);
|
||||
});
|
||||
|
||||
telemetry::event!(
|
||||
"Agent Token Limit Warning",
|
||||
agent = agent_telemetry_id,
|
||||
session_id = session_id,
|
||||
kind = kind,
|
||||
);
|
||||
}
|
||||
|
||||
fn emit_load_error_telemetry(&self, error: &LoadError) {
|
||||
let error_kind = match error {
|
||||
LoadError::Unsupported { .. } => "unsupported",
|
||||
LoadError::FailedToInstall(_) => "failed_to_install",
|
||||
LoadError::Exited { .. } => "exited",
|
||||
LoadError::Other(_) => "other",
|
||||
};
|
||||
|
||||
let agent_name = self.agent.name();
|
||||
|
||||
telemetry::event!(
|
||||
"Agent Panel Error Shown",
|
||||
agent = agent_name,
|
||||
kind = error_kind,
|
||||
message = error.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
fn render_load_error(
|
||||
&self,
|
||||
e: &LoadError,
|
||||
|
||||
@@ -180,6 +180,7 @@ pub struct AcpThreadView {
|
||||
pub(super) thread_error: Option<ThreadError>,
|
||||
pub thread_error_markdown: Option<Entity<Markdown>>,
|
||||
pub token_limit_callout_dismissed: bool,
|
||||
pub last_token_limit_telemetry: Option<acp_thread::TokenUsageRatio>,
|
||||
thread_feedback: ThreadFeedbackState,
|
||||
pub list_state: ListState,
|
||||
pub prompt_capabilities: Rc<RefCell<PromptCapabilities>>,
|
||||
@@ -356,6 +357,7 @@ impl AcpThreadView {
|
||||
thread_error: None,
|
||||
thread_error_markdown: None,
|
||||
token_limit_callout_dismissed: false,
|
||||
last_token_limit_telemetry: None,
|
||||
thread_feedback: Default::default(),
|
||||
expanded_tool_calls: HashSet::default(),
|
||||
expanded_tool_call_raw_inputs: HashSet::default(),
|
||||
|
||||
@@ -440,6 +440,7 @@ pub struct AgentPanel {
|
||||
onboarding: Entity<AgentPanelOnboarding>,
|
||||
selected_agent: AgentType,
|
||||
show_trust_workspace_message: bool,
|
||||
last_configuration_error_telemetry: Option<String>,
|
||||
}
|
||||
|
||||
impl AgentPanel {
|
||||
@@ -660,6 +661,7 @@ impl AgentPanel {
|
||||
thread_store,
|
||||
selected_agent: AgentType::default(),
|
||||
show_trust_workspace_message: false,
|
||||
last_configuration_error_telemetry: None,
|
||||
};
|
||||
|
||||
// Initial sync of agent servers from extensions
|
||||
@@ -2694,6 +2696,33 @@ impl AgentPanel {
|
||||
)
|
||||
}
|
||||
|
||||
fn emit_configuration_error_telemetry_if_needed(
|
||||
&mut self,
|
||||
configuration_error: Option<&ConfigurationError>,
|
||||
) {
|
||||
let error_kind = configuration_error.map(|err| match err {
|
||||
ConfigurationError::NoProvider => "no_provider",
|
||||
ConfigurationError::ModelNotFound => "model_not_found",
|
||||
ConfigurationError::ProviderNotAuthenticated(_) => "provider_not_authenticated",
|
||||
});
|
||||
|
||||
let error_kind_string = error_kind.map(String::from);
|
||||
|
||||
if self.last_configuration_error_telemetry == error_kind_string {
|
||||
return;
|
||||
}
|
||||
|
||||
self.last_configuration_error_telemetry = error_kind_string;
|
||||
|
||||
if let Some(kind) = error_kind {
|
||||
let message = configuration_error
|
||||
.map(|err| err.to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
telemetry::event!("Agent Panel Error Shown", kind = kind, message = message,);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_configuration_error(
|
||||
&self,
|
||||
border_bottom: bool,
|
||||
@@ -2978,46 +3007,57 @@ impl Render for AgentPanel {
|
||||
.child(self.render_toolbar(window, cx))
|
||||
.children(self.render_workspace_trust_message(cx))
|
||||
.children(self.render_onboarding(window, cx))
|
||||
.map(|parent| match &self.active_view {
|
||||
ActiveView::Uninitialized => parent,
|
||||
ActiveView::AgentThread { thread_view, .. } => parent
|
||||
.child(thread_view.clone())
|
||||
.child(self.render_drag_target(cx)),
|
||||
ActiveView::History { kind } => match kind {
|
||||
HistoryKind::AgentThreads => parent.child(self.acp_history.clone()),
|
||||
HistoryKind::TextThreads => parent.child(self.text_thread_history.clone()),
|
||||
},
|
||||
ActiveView::TextThread {
|
||||
text_thread_editor,
|
||||
buffer_search_bar,
|
||||
..
|
||||
} => {
|
||||
.map(|parent| {
|
||||
// Emit configuration error telemetry before entering the match to avoid borrow conflicts
|
||||
if matches!(&self.active_view, ActiveView::TextThread { .. }) {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let configuration_error =
|
||||
model_registry.configuration_error(model_registry.default_model(), cx);
|
||||
parent
|
||||
.map(|this| {
|
||||
if !self.should_render_onboarding(cx)
|
||||
&& let Some(err) = configuration_error.as_ref()
|
||||
{
|
||||
this.child(self.render_configuration_error(
|
||||
true,
|
||||
err,
|
||||
&self.focus_handle(cx),
|
||||
cx,
|
||||
))
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
.child(self.render_text_thread(
|
||||
text_thread_editor,
|
||||
buffer_search_bar,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
self.emit_configuration_error_telemetry_if_needed(configuration_error.as_ref());
|
||||
}
|
||||
|
||||
match &self.active_view {
|
||||
ActiveView::Uninitialized => parent,
|
||||
ActiveView::AgentThread { thread_view, .. } => parent
|
||||
.child(thread_view.clone())
|
||||
.child(self.render_drag_target(cx)),
|
||||
ActiveView::History { kind } => match kind {
|
||||
HistoryKind::AgentThreads => parent.child(self.acp_history.clone()),
|
||||
HistoryKind::TextThreads => parent.child(self.text_thread_history.clone()),
|
||||
},
|
||||
ActiveView::TextThread {
|
||||
text_thread_editor,
|
||||
buffer_search_bar,
|
||||
..
|
||||
} => {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let configuration_error =
|
||||
model_registry.configuration_error(model_registry.default_model(), cx);
|
||||
|
||||
parent
|
||||
.map(|this| {
|
||||
if !self.should_render_onboarding(cx)
|
||||
&& let Some(err) = configuration_error.as_ref()
|
||||
{
|
||||
this.child(self.render_configuration_error(
|
||||
true,
|
||||
err,
|
||||
&self.focus_handle(cx),
|
||||
cx,
|
||||
))
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
.child(self.render_text_thread(
|
||||
text_thread_editor,
|
||||
buffer_search_bar,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
ActiveView::Configuration => parent.children(self.configuration.clone()),
|
||||
}
|
||||
ActiveView::Configuration => parent.children(self.configuration.clone()),
|
||||
})
|
||||
.children(self.render_trial_end_upsell(window, cx));
|
||||
|
||||
|
||||
Reference in new issue
Block a user