telemetry.rs

  1use crate::ANTHROPIC_PROVIDER_ID;
  2use anthropic::ANTHROPIC_API_URL;
  3use anyhow::{Context as _, anyhow};
  4use gpui::BackgroundExecutor;
  5use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
  6use std::env;
  7use std::sync::Arc;
  8use util::ResultExt;
  9
 10#[derive(Clone, Debug)]
 11pub struct AnthropicEventData {
 12    pub completion_type: AnthropicCompletionType,
 13    pub event: AnthropicEventType,
 14    pub language_name: Option<String>,
 15    pub message_id: Option<String>,
 16}
 17
 18#[derive(Clone, Debug)]
 19pub enum AnthropicCompletionType {
 20    Editor,
 21    Terminal,
 22    Panel,
 23}
 24
 25#[derive(Clone, Debug)]
 26pub enum AnthropicEventType {
 27    Invoked,
 28    Response,
 29    Accept,
 30    Reject,
 31}
 32
 33impl AnthropicCompletionType {
 34    fn as_str(&self) -> &'static str {
 35        match self {
 36            Self::Editor => "natural_language_completion_in_editor",
 37            Self::Terminal => "natural_language_completion_in_terminal",
 38            Self::Panel => "conversation_message",
 39        }
 40    }
 41}
 42
 43impl AnthropicEventType {
 44    fn as_str(&self) -> &'static str {
 45        match self {
 46            Self::Invoked => "invoke",
 47            Self::Response => "response",
 48            Self::Accept => "accept",
 49            Self::Reject => "reject",
 50        }
 51    }
 52}
 53
 54pub fn report_anthropic_event(
 55    model: &Arc<dyn crate::LanguageModel>,
 56    event: AnthropicEventData,
 57    cx: &gpui::App,
 58) {
 59    let reporter = AnthropicEventReporter::new(model, cx);
 60    reporter.report(event);
 61}
 62
 63#[derive(Clone)]
 64pub struct AnthropicEventReporter {
 65    http_client: Arc<dyn HttpClient>,
 66    executor: BackgroundExecutor,
 67    api_key: Option<String>,
 68    is_anthropic: bool,
 69}
 70
 71impl AnthropicEventReporter {
 72    pub fn new(model: &Arc<dyn crate::LanguageModel>, cx: &gpui::App) -> Self {
 73        Self {
 74            http_client: cx.http_client(),
 75            executor: cx.background_executor().clone(),
 76            api_key: model.api_key(cx),
 77            is_anthropic: model.provider_id() == ANTHROPIC_PROVIDER_ID,
 78        }
 79    }
 80
 81    pub fn report(&self, event: AnthropicEventData) {
 82        if !self.is_anthropic {
 83            return;
 84        }
 85        let Some(api_key) = self.api_key.clone() else {
 86            return;
 87        };
 88        let client = self.http_client.clone();
 89        self.executor
 90            .spawn(async move {
 91                send_anthropic_event(event, client, api_key).await.log_err();
 92            })
 93            .detach();
 94    }
 95}
 96
 97async fn send_anthropic_event(
 98    event: AnthropicEventData,
 99    client: Arc<dyn HttpClient>,
100    api_key: String,
101) -> anyhow::Result<()> {
102    let uri = format!("{ANTHROPIC_API_URL}/v1/log/zed");
103    let request_builder = HttpRequest::builder()
104        .method(Method::POST)
105        .uri(uri)
106        .header("X-Api-Key", api_key)
107        .header("Content-Type", "application/json");
108
109    let serialized_event = serde_json::json!({
110        "completion_type": event.completion_type.as_str(),
111        "event": event.event.as_str(),
112        "metadata": {
113            "language_name": event.language_name,
114            "message_id": event.message_id,
115            "platform": env::consts::OS,
116        }
117    });
118
119    let request = request_builder
120        .body(AsyncBody::from(serialized_event.to_string()))
121        .context("Failed to construct Anthropic telemetry HTTP request body")?;
122
123    let response = client
124        .send(request)
125        .await
126        .context("Failed to send telemetry HTTP request to Anthropic")?;
127
128    if response.status().is_success() {
129        Ok(())
130    } else {
131        Err(anyhow!(
132            "Anthropic telemetry logging failed with HTTP status: {}",
133            response.status()
134        ))
135    }
136}