acp: Add telemetry (#36894)

Bennet Bo Fenner and Conrad Irwin created

Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

crates/agent2/src/native_agent_server.rs  |  4 +
crates/agent2/src/tests/mod.rs            |  1 
crates/agent_servers/src/agent_servers.rs |  1 
crates/agent_servers/src/claude.rs        |  4 +
crates/agent_servers/src/custom.rs        |  4 +
crates/agent_servers/src/gemini.rs        |  4 +
crates/agent_ui/src/acp/thread_view.rs    | 79 ++++++++++++++++++------
crates/agent_ui/src/agent_panel.rs        |  8 ++
crates/agent_ui/src/agent_ui.rs           |  9 ++
crates/agent_ui/src/text_thread_editor.rs |  1 
10 files changed, 93 insertions(+), 22 deletions(-)

Detailed changes

crates/agent2/src/native_agent_server.rs 🔗

@@ -22,6 +22,10 @@ impl NativeAgentServer {
 }
 
 impl AgentServer for NativeAgentServer {
+    fn telemetry_id(&self) -> &'static str {
+        "zed"
+    }
+
     fn name(&self) -> SharedString {
         "Zed Agent".into()
     }

crates/agent2/src/tests/mod.rs 🔗

@@ -1685,6 +1685,7 @@ async fn test_truncate_second_message(cx: &mut TestAppContext) {
 }
 
 #[gpui::test]
+#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
 async fn test_title_generation(cx: &mut TestAppContext) {
     let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
     let fake_model = model.as_fake();

crates/agent_servers/src/agent_servers.rs 🔗

@@ -36,6 +36,7 @@ pub trait AgentServer: Send {
     fn name(&self) -> SharedString;
     fn empty_state_headline(&self) -> SharedString;
     fn empty_state_message(&self) -> SharedString;
+    fn telemetry_id(&self) -> &'static str;
 
     fn connect(
         &self,

crates/agent_servers/src/claude.rs 🔗

@@ -43,6 +43,10 @@ use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri
 pub struct ClaudeCode;
 
 impl AgentServer for ClaudeCode {
+    fn telemetry_id(&self) -> &'static str {
+        "claude-code"
+    }
+
     fn name(&self) -> SharedString {
         "Claude Code".into()
     }

crates/agent_servers/src/custom.rs 🔗

@@ -22,6 +22,10 @@ impl CustomAgentServer {
 }
 
 impl crate::AgentServer for CustomAgentServer {
+    fn telemetry_id(&self) -> &'static str {
+        "custom"
+    }
+
     fn name(&self) -> SharedString {
         self.name.clone()
     }

crates/agent_servers/src/gemini.rs 🔗

@@ -17,6 +17,10 @@ pub struct Gemini;
 const ACP_ARG: &str = "--experimental-acp";
 
 impl AgentServer for Gemini {
+    fn telemetry_id(&self) -> &'static str {
+        "gemini-cli"
+    }
+
     fn name(&self) -> SharedString {
         "Gemini CLI".into()
     }

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -892,6 +892,8 @@ impl AcpThreadView {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
+        let agent_telemetry_id = self.agent.telemetry_id();
+
         self.thread_error.take();
         self.editing_message.take();
         self.thread_feedback.clear();
@@ -936,6 +938,9 @@ impl AcpThreadView {
                     }
                 });
                 drop(guard);
+
+                telemetry::event!("Agent Message Sent", agent = agent_telemetry_id);
+
                 thread.send(contents, cx)
             })?;
             send.await
@@ -1246,30 +1251,44 @@ impl AcpThreadView {
         pending_auth_method.replace(method.clone());
         let authenticate = connection.authenticate(method, cx);
         cx.notify();
-        self.auth_task = Some(cx.spawn_in(window, {
-            let project = self.project.clone();
-            let agent = self.agent.clone();
-            async move |this, cx| {
-                let result = authenticate.await;
+        self.auth_task =
+            Some(cx.spawn_in(window, {
+                let project = self.project.clone();
+                let agent = self.agent.clone();
+                async move |this, cx| {
+                    let result = authenticate.await;
 
-                this.update_in(cx, |this, window, cx| {
-                    if let Err(err) = result {
-                        this.handle_thread_error(err, cx);
-                    } else {
-                        this.thread_state = Self::initial_state(
-                            agent,
-                            None,
-                            this.workspace.clone(),
-                            project.clone(),
-                            window,
-                            cx,
-                        )
+                    match &result {
+                        Ok(_) => telemetry::event!(
+                            "Authenticate Agent Succeeded",
+                            agent = agent.telemetry_id()
+                        ),
+                        Err(_) => {
+                            telemetry::event!(
+                                "Authenticate Agent Failed",
+                                agent = agent.telemetry_id(),
+                            )
+                        }
                     }
-                    this.auth_task.take()
-                })
-                .ok();
-            }
-        }));
+
+                    this.update_in(cx, |this, window, cx| {
+                        if let Err(err) = result {
+                            this.handle_thread_error(err, cx);
+                        } else {
+                            this.thread_state = Self::initial_state(
+                                agent,
+                                None,
+                                this.workspace.clone(),
+                                project.clone(),
+                                window,
+                                cx,
+                            )
+                        }
+                        this.auth_task.take()
+                    })
+                    .ok();
+                }
+            }));
     }
 
     fn authorize_tool_call(
@@ -2776,6 +2795,12 @@ impl AcpThreadView {
                                         .on_click({
                                             let method_id = method.id.clone();
                                             cx.listener(move |this, _, window, cx| {
+                                                telemetry::event!(
+                                                    "Authenticate Agent Started",
+                                                    agent = this.agent.telemetry_id(),
+                                                    method = method_id
+                                                );
+
                                                 this.authenticate(method_id.clone(), window, cx)
                                             })
                                         })
@@ -2804,6 +2829,8 @@ impl AcpThreadView {
                     .icon_color(Color::Muted)
                     .icon_position(IconPosition::Start)
                     .on_click(cx.listener(move |this, _, window, cx| {
+                        telemetry::event!("Agent Install CLI", agent = this.agent.telemetry_id());
+
                         let task = this
                             .workspace
                             .update(cx, |workspace, cx| {
@@ -2861,6 +2888,8 @@ impl AcpThreadView {
                     .icon_color(Color::Muted)
                     .icon_position(IconPosition::Start)
                     .on_click(cx.listener(move |this, _, window, cx| {
+                        telemetry::event!("Agent Upgrade CLI", agent = this.agent.telemetry_id());
+
                         let task = this
                             .workspace
                             .update(cx, |workspace, cx| {
@@ -3708,6 +3737,8 @@ impl AcpThreadView {
                 }
             })
             .ok();
+
+        telemetry::event!("Follow Agent Selected", following = !following);
     }
 
     fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
@@ -5323,6 +5354,10 @@ pub(crate) mod tests {
     where
         C: 'static + AgentConnection + Send + Clone,
     {
+        fn telemetry_id(&self) -> &'static str {
+            "test"
+        }
+
         fn logo(&self) -> ui::IconName {
             ui::IconName::Ai
         }

crates/agent_ui/src/agent_panel.rs 🔗

@@ -1026,6 +1026,8 @@ impl AgentPanel {
     }
 
     fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        telemetry::event!("Agent Thread Started", agent = "zed-text");
+
         let context = self
             .context_store
             .update(cx, |context_store, cx| context_store.create(cx));
@@ -1118,6 +1120,8 @@ impl AgentPanel {
                 }
             };
 
+            telemetry::event!("Agent Thread Started", agent = ext_agent.name());
+
             let server = ext_agent.server(fs, history);
 
             this.update_in(cx, |this, window, cx| {
@@ -2327,6 +2331,8 @@ impl AgentPanel {
             .menu({
                 let menu = self.assistant_navigation_menu.clone();
                 move |window, cx| {
+                    telemetry::event!("View Thread History Clicked");
+
                     if let Some(menu) = menu.as_ref() {
                         menu.update(cx, |_, cx| {
                             cx.defer_in(window, |menu, window, cx| {
@@ -2505,6 +2511,8 @@ impl AgentPanel {
                 let workspace = self.workspace.clone();
 
                 move |window, cx| {
+                    telemetry::event!("New Thread Clicked");
+
                     let active_thread = active_thread.clone();
                     Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
                         menu = menu

crates/agent_ui/src/agent_ui.rs 🔗

@@ -175,6 +175,15 @@ enum ExternalAgent {
 }
 
 impl ExternalAgent {
+    fn name(&self) -> &'static str {
+        match self {
+            Self::NativeAgent => "zed",
+            Self::Gemini => "gemini-cli",
+            Self::ClaudeCode => "claude-code",
+            Self::Custom { .. } => "custom",
+        }
+    }
+
     pub fn server(
         &self,
         fs: Arc<dyn fs::Fs>,

crates/agent_ui/src/text_thread_editor.rs 🔗

@@ -361,6 +361,7 @@ impl TextThreadEditor {
         if self.sending_disabled(cx) {
             return;
         }
+        telemetry::event!("Agent Message Sent", agent = "zed-text");
         self.send_to_model(window, cx);
     }