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:
authored and GitHub committed 2026-02-05 14:21:28 -08:00
1 parent 9c3526f2c4
commit 955581bdfd
4 files changed
+157 -42

No files matched your search

+1 -1
View File
@@ -932,7 +932,7 @@ impl TokenUsage {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum TokenUsageRatio {
Normal,
Warning,
+78 -5
View File
@@ -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(),
+76 -36
View File
@@ -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));