From f19ab464c7b0610bc1479195ca86572c8205d700 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 23 Feb 2024 09:13:28 -0700 Subject: [PATCH] Add telemetry events backend for collab (#8220) Send telemetry to collab not zed.dev Release Notes: - N/A --------- Co-authored-by: Marshall Co-authored-by: Marshall Bowers --- Cargo.lock | 89 ++ Cargo.toml | 5 + crates/assistant/Cargo.toml | 1 + crates/assistant/src/assistant_panel.rs | 2 +- crates/client/Cargo.toml | 3 +- crates/client/src/client.rs | 2 +- crates/client/src/telemetry.rs | 237 ++---- crates/collab/.env.toml | 6 + crates/collab/Cargo.toml | 4 + crates/collab/src/api.rs | 1 + crates/collab/src/api/events.rs | 805 ++++++++++++++++++ crates/collab/src/lib.rs | 50 +- crates/collab/src/main.rs | 1 + crates/collab/src/tests/test_server.rs | 6 + crates/telemetry_events/Cargo.toml | 13 + crates/telemetry_events/LICENSE-GPL | 1 + .../telemetry_events/src/telemetry_events.rs | 131 +++ crates/util/src/http.rs | 13 + typos.toml | 3 + 19 files changed, 1196 insertions(+), 177 deletions(-) create mode 100644 crates/collab/src/api/events.rs create mode 100644 crates/telemetry_events/Cargo.toml create mode 120000 crates/telemetry_events/LICENSE-GPL create mode 100644 crates/telemetry_events/src/telemetry_events.rs diff --git a/Cargo.lock b/Cargo.lock index ea81395ab8..c531f10753 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -360,6 +360,7 @@ dependencies = [ "serde_json", "settings", "smol", + "telemetry_events", "theme", "tiktoken-rs", "ui", @@ -1903,6 +1904,49 @@ dependencies = [ "util", ] +[[package]] +name = "clickhouse" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0875e527e299fc5f4faba42870bf199a39ab0bb2dbba1b8aef0a2151451130f" +dependencies = [ + "bstr", + "bytes 1.5.0", + "clickhouse-derive", + "clickhouse-rs-cityhash-sys", + "futures 0.3.28", + "hyper", + "hyper-tls", + "lz4", + "sealed", + "serde", + "static_assertions", + "thiserror", + "tokio", + "url", +] + +[[package]] +name = "clickhouse-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18af5425854858c507eec70f7deb4d5d8cec4216fcb086283a78872387281ea5" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 1.0.109", +] + +[[package]] +name = "clickhouse-rs-cityhash-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4baf9d4700a28d6cb600e17ed6ae2b43298a5245f1f76b4eab63027ebfd592b9" +dependencies = [ + "cc", +] + [[package]] name = "client" version = "0.1.0" @@ -1935,6 +1979,7 @@ dependencies = [ "smol", "sum_tree", "sysinfo", + "telemetry_events", "tempfile", "text", "thiserror", @@ -2020,6 +2065,7 @@ dependencies = [ "channel", "chrono", "clap 3.2.25", + "clickhouse", "client", "clock", "collab_ui", @@ -2034,6 +2080,7 @@ dependencies = [ "futures 0.3.28", "git", "gpui", + "hex", "hyper", "indoc", "language", @@ -2064,8 +2111,10 @@ dependencies = [ "serde_json", "settings", "sha-1 0.9.8", + "sha2 0.10.7", "smallvec", "sqlx", + "telemetry_events", "text", "theme", "time", @@ -5342,6 +5391,26 @@ dependencies = [ "url", ] +[[package]] +name = "lz4" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9e2dd86df36ce760a60f6ff6ad526f7ba1f14ba0356f8254fb6905e6494df1" +dependencies = [ + "libc", + "lz4-sys", +] + +[[package]] +name = "lz4-sys" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d27b317e207b10f69f5e75494119e391a96f48861ae870d1da6edac98ca900" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "mac" version = "0.1.1" @@ -8282,6 +8351,18 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "sealed" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b5e421024b5e5edfbaa8e60ecf90bda9dbffc602dbb230e6028763f85f0c68c" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "search" version = "0.1.0" @@ -9489,6 +9570,14 @@ dependencies = [ "workspace", ] +[[package]] +name = "telemetry_events" +version = "0.1.0" +dependencies = [ + "serde", + "util", +] + [[package]] name = "tempfile" version = "3.9.0" diff --git a/Cargo.toml b/Cargo.toml index 2df6930078..2d600bb086 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,6 +81,7 @@ members = [ "crates/theme", "crates/theme_importer", "crates/theme_selector", + "crates/telemetry_events", "crates/ui", "crates/util", "crates/vcs_menu", @@ -173,6 +174,7 @@ text = { path = "crates/text" } theme = { path = "crates/theme" } theme_importer = { path = "crates/theme_importer" } theme_selector = { path = "crates/theme_selector" } +telemetry_events = { path ="crates/telemetry_events" } ui = { path = "crates/ui" } util = { path = "crates/util" } vcs_menu = { path = "crates/vcs_menu" } @@ -190,12 +192,14 @@ blade-graphics = { git = "https://github.com/kvark/blade", rev = "e9d93a4d41f394 blade-macros = { git = "https://github.com/kvark/blade", rev = "e9d93a4d41f3946a03ffb76136290d6ccf7f2b80" } blade-rwh = { package = "raw-window-handle", version = "0.5" } chrono = { version = "0.4", features = ["serde"] } +clickhouse = { version = "0.11.6" } ctor = "0.2.6" derive_more = "0.99.17" env_logger = "0.9" futures = "0.3" git2 = { version = "0.15", default-features = false } globset = "0.4" +hex = "0.4.3" indoc = "1" # We explicitly disable a http2 support in isahc. isahc = { version = "1.7.2", default-features = false, features = ["static-curl", "text-decoding"] } @@ -221,6 +225,7 @@ serde_derive = { version = "1.0", features = ["deserialize_in_place"] } serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } serde_json_lenient = { version = "0.1", features = ["preserve_order", "raw_value"] } serde_repr = "0.1" +sha2 = "0.10" smallvec = { version = "1.6", features = ["union"] } smol = "1.2" strum = { version = "0.25.0", features = ["derive"] } diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index 97e1a13765..2b0e74f941 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -36,6 +36,7 @@ serde.workspace = true serde_json.workspace = true settings.workspace = true smol.workspace = true +telemetry_events.workspace = true theme.workspace = true tiktoken-rs.workspace = true ui.workspace = true diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 76f66cd4fe..9a04f016ba 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -15,7 +15,6 @@ use ai::{ }; use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; -use client::telemetry::AssistantKind; use collections::{hash_map, HashMap, HashSet, VecDeque}; use editor::{ actions::{MoveDown, MoveUp}, @@ -52,6 +51,7 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; +use telemetry_events::AssistantKind; use theme::ThemeSettings; use ui::{ prelude::*, diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 6e3660f5f2..5ecd14b257 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -41,9 +41,10 @@ schemars.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true -sha2 = "0.10" +sha2.workspace = true smol.workspace = true sysinfo.workspace = true +telemetry_events.workspace = true tempfile.workspace = true thiserror.workspace = true time.workspace = true diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 04d431b150..a50d96eed8 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -46,7 +46,7 @@ use util::http::{HttpClient, ZedHttpClient}; use util::{ResultExt, TryFutureExt}; pub use rpc::*; -pub use telemetry::Event; +pub use telemetry_events::Event; pub use user::*; lazy_static! { diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 085ad2cb67..2bee99073c 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -8,7 +8,6 @@ use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task}; use once_cell::sync::Lazy; use parking_lot::Mutex; use release_channel::ReleaseChannel; -use serde::Serialize; use settings::{Settings, SettingsStore}; use sha2::{Digest, Sha256}; use std::io::Write; @@ -16,6 +15,10 @@ use std::{env, mem, path::PathBuf, sync::Arc, time::Duration}; use sysinfo::{ CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt, }; +use telemetry_events::{ + ActionEvent, AppEvent, AssistantEvent, AssistantKind, CallEvent, CopilotEvent, CpuEvent, + EditEvent, EditorEvent, Event, EventRequestBody, EventWrapper, MemoryEvent, SettingEvent, +}; use tempfile::NamedTempFile; use util::http::{self, HttpClient, Method, ZedHttpClient}; #[cfg(not(debug_assertions))] @@ -35,7 +38,7 @@ struct TelemetryState { settings: TelemetrySettings, metrics_id: Option>, // Per logged-in user installation_id: Option>, // Per app installation (different for dev, nightly, preview, and stable) - session_id: Option>, // Per app launch + session_id: Option, // Per app launch release_channel: Option<&'static str>, app_metadata: AppMetadata, architecture: &'static str, @@ -48,93 +51,6 @@ struct TelemetryState { max_queue_size: usize, } -#[derive(Serialize, Debug)] -struct EventRequestBody { - installation_id: Option>, - session_id: Option>, - is_staff: Option, - app_version: Option, - os_name: &'static str, - os_version: Option, - architecture: &'static str, - release_channel: Option<&'static str>, - events: Vec, -} - -#[derive(Serialize, Debug)] -struct EventWrapper { - signed_in: bool, - #[serde(flatten)] - event: Event, -} - -#[derive(Clone, Debug, PartialEq, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum AssistantKind { - Panel, - Inline, -} - -#[derive(Clone, Debug, PartialEq, Serialize)] -#[serde(tag = "type")] -pub enum Event { - Editor { - operation: &'static str, - file_extension: Option, - vim_mode: bool, - copilot_enabled: bool, - copilot_enabled_for_language: bool, - milliseconds_since_first_event: i64, - }, - Copilot { - suggestion_id: Option, - suggestion_accepted: bool, - file_extension: Option, - milliseconds_since_first_event: i64, - }, - Call { - operation: &'static str, - room_id: Option, - channel_id: Option, - milliseconds_since_first_event: i64, - }, - Assistant { - conversation_id: Option, - kind: AssistantKind, - model: &'static str, - milliseconds_since_first_event: i64, - }, - Cpu { - usage_as_percentage: f32, - core_count: u32, - milliseconds_since_first_event: i64, - }, - Memory { - memory_in_bytes: u64, - virtual_memory_in_bytes: u64, - milliseconds_since_first_event: i64, - }, - App { - operation: String, - milliseconds_since_first_event: i64, - }, - Setting { - setting: &'static str, - value: String, - milliseconds_since_first_event: i64, - }, - Edit { - duration: i64, - environment: &'static str, - milliseconds_since_first_event: i64, - }, - Action { - source: &'static str, - action: String, - milliseconds_since_first_event: i64, - }, -} - #[cfg(debug_assertions)] const MAX_QUEUE_LEN: usize = 5; @@ -146,7 +62,6 @@ const FLUSH_INTERVAL: Duration = Duration::from_secs(1); #[cfg(not(debug_assertions))] const FLUSH_INTERVAL: Duration = Duration::from_secs(60 * 5); - static ZED_CLIENT_CHECKSUM_SEED: Lazy>> = Lazy::new(|| { option_env!("ZED_CLIENT_CHECKSUM_SEED") .map(|s| s.as_bytes().into()) @@ -318,15 +233,13 @@ impl Telemetry { copilot_enabled: bool, copilot_enabled_for_language: bool, ) { - let event = Event::Editor { + let event = Event::Editor(EditorEvent { file_extension, vim_mode, - operation, + operation: operation.into(), copilot_enabled, copilot_enabled_for_language, - milliseconds_since_first_event: self - .milliseconds_since_first_event(self.clock.utc_now()), - }; + }); self.report_event(event) } @@ -337,13 +250,11 @@ impl Telemetry { suggestion_accepted: bool, file_extension: Option, ) { - let event = Event::Copilot { + let event = Event::Copilot(CopilotEvent { suggestion_id, suggestion_accepted, file_extension, - milliseconds_since_first_event: self - .milliseconds_since_first_event(self.clock.utc_now()), - }; + }); self.report_event(event) } @@ -354,13 +265,11 @@ impl Telemetry { kind: AssistantKind, model: &'static str, ) { - let event = Event::Assistant { + let event = Event::Assistant(AssistantEvent { conversation_id, kind, - model, - milliseconds_since_first_event: self - .milliseconds_since_first_event(self.clock.utc_now()), - }; + model: model.to_string(), + }); self.report_event(event) } @@ -371,24 +280,20 @@ impl Telemetry { room_id: Option, channel_id: Option, ) { - let event = Event::Call { - operation, + let event = Event::Call(CallEvent { + operation: operation.to_string(), room_id, channel_id, - milliseconds_since_first_event: self - .milliseconds_since_first_event(self.clock.utc_now()), - }; + }); self.report_event(event) } pub fn report_cpu_event(self: &Arc, usage_as_percentage: f32, core_count: u32) { - let event = Event::Cpu { + let event = Event::Cpu(CpuEvent { usage_as_percentage, core_count, - milliseconds_since_first_event: self - .milliseconds_since_first_event(self.clock.utc_now()), - }; + }); self.report_event(event) } @@ -398,22 +303,16 @@ impl Telemetry { memory_in_bytes: u64, virtual_memory_in_bytes: u64, ) { - let event = Event::Memory { + let event = Event::Memory(MemoryEvent { memory_in_bytes, virtual_memory_in_bytes, - milliseconds_since_first_event: self - .milliseconds_since_first_event(self.clock.utc_now()), - }; + }); self.report_event(event) } pub fn report_app_event(self: &Arc, operation: String) -> Event { - let event = Event::App { - operation, - milliseconds_since_first_event: self - .milliseconds_since_first_event(self.clock.utc_now()), - }; + let event = Event::App(AppEvent { operation }); self.report_event(event.clone()); @@ -421,12 +320,10 @@ impl Telemetry { } pub fn report_setting_event(self: &Arc, setting: &'static str, value: String) { - let event = Event::Setting { - setting, + let event = Event::Setting(SettingEvent { + setting: setting.to_string(), value, - milliseconds_since_first_event: self - .milliseconds_since_first_event(self.clock.utc_now()), - }; + }); self.report_event(event) } @@ -437,42 +334,24 @@ impl Telemetry { drop(state); if let Some((start, end, environment)) = period_data { - let event = Event::Edit { + let event = Event::Edit(EditEvent { duration: end.timestamp_millis() - start.timestamp_millis(), - environment, - milliseconds_since_first_event: self - .milliseconds_since_first_event(self.clock.utc_now()), - }; + environment: environment.to_string(), + }); self.report_event(event); } } pub fn report_action_event(self: &Arc, source: &'static str, action: String) { - let event = Event::Action { - source, + let event = Event::Action(ActionEvent { + source: source.to_string(), action, - milliseconds_since_first_event: self - .milliseconds_since_first_event(self.clock.utc_now()), - }; + }); self.report_event(event) } - fn milliseconds_since_first_event(self: &Arc, date_time: DateTime) -> i64 { - let mut state = self.state.lock(); - - match state.first_event_date_time { - Some(first_event_date_time) => { - date_time.timestamp_millis() - first_event_date_time.timestamp_millis() - } - None => { - state.first_event_date_time = Some(date_time); - 0 - } - } - } - fn report_event(self: &Arc, event: Event) { let mut state = self.state.lock(); @@ -489,8 +368,24 @@ impl Telemetry { })); } + let date_time = self.clock.utc_now(); + + let milliseconds_since_first_event = match state.first_event_date_time { + Some(first_event_date_time) => { + date_time.timestamp_millis() - first_event_date_time.timestamp_millis() + } + None => { + state.first_event_date_time = Some(date_time); + 0 + } + }; + let signed_in = state.metrics_id.is_some(); - state.events_queue.push(EventWrapper { signed_in, event }); + state.events_queue.push(EventWrapper { + signed_in, + milliseconds_since_first_event, + event, + }); if state.installation_id.is_some() { if state.events_queue.len() >= state.max_queue_size { @@ -545,21 +440,22 @@ impl Telemetry { { let state = this.state.lock(); let request_body = EventRequestBody { - installation_id: state.installation_id.clone(), + installation_id: state.installation_id.as_deref().map(Into::into), session_id: state.session_id.clone(), is_staff: state.is_staff.clone(), app_version: state .app_metadata .app_version - .map(|version| version.to_string()), - os_name: state.app_metadata.os_name, + .unwrap_or_default() + .to_string(), + os_name: state.app_metadata.os_name.to_string(), os_version: state .app_metadata .os_version .map(|version| version.to_string()), - architecture: state.architecture, + architecture: state.architecture.to_string(), - release_channel: state.release_channel, + release_channel: state.release_channel.map(Into::into), events, }; json_bytes.clear(); @@ -578,7 +474,7 @@ impl Telemetry { let request = http::Request::builder() .method(Method::POST) - .uri(&this.http_client.zed_url("/api/events")) + .uri(this.http_client.zed_api_url("/telemetry/events")) .header("Content-Type", "text/plain") .header("x-zed-checksum", checksum) .body(json_bytes.into()); @@ -627,10 +523,9 @@ mod tests { let event = telemetry.report_app_event(operation.clone()); assert_eq!( event, - Event::App { + Event::App(AppEvent { operation: operation.clone(), - milliseconds_since_first_event: 0 - } + }) ); assert_eq!(telemetry.state.lock().events_queue.len(), 1); assert!(telemetry.state.lock().flush_events_task.is_some()); @@ -644,10 +539,9 @@ mod tests { let event = telemetry.report_app_event(operation.clone()); assert_eq!( event, - Event::App { + Event::App(AppEvent { operation: operation.clone(), - milliseconds_since_first_event: 100 - } + }) ); assert_eq!(telemetry.state.lock().events_queue.len(), 2); assert!(telemetry.state.lock().flush_events_task.is_some()); @@ -661,10 +555,9 @@ mod tests { let event = telemetry.report_app_event(operation.clone()); assert_eq!( event, - Event::App { + Event::App(AppEvent { operation: operation.clone(), - milliseconds_since_first_event: 200 - } + }) ); assert_eq!(telemetry.state.lock().events_queue.len(), 3); assert!(telemetry.state.lock().flush_events_task.is_some()); @@ -679,10 +572,9 @@ mod tests { let event = telemetry.report_app_event(operation.clone()); assert_eq!( event, - Event::App { + Event::App(AppEvent { operation: operation.clone(), - milliseconds_since_first_event: 300 - } + }) ); assert!(is_empty_state(&telemetry)); @@ -712,10 +604,9 @@ mod tests { let event = telemetry.report_app_event(operation.clone()); assert_eq!( event, - Event::App { + Event::App(AppEvent { operation: operation.clone(), - milliseconds_since_first_event: 0 - } + }) ); assert_eq!(telemetry.state.lock().events_queue.len(), 1); assert!(telemetry.state.lock().flush_events_task.is_some()); diff --git a/crates/collab/.env.toml b/crates/collab/.env.toml index 7340a71cd9..091e0d5a82 100644 --- a/crates/collab/.env.toml +++ b/crates/collab/.env.toml @@ -12,6 +12,12 @@ BLOB_STORE_SECRET_KEY = "the-blob-store-secret-key" BLOB_STORE_BUCKET = "the-extensions-bucket" BLOB_STORE_URL = "http://127.0.0.1:9000" BLOB_STORE_REGION = "the-region" +ZED_CLIENT_CHECKSUM_SEED = "development-client-checksum-seed" + +CLICKHOUSE_URL = "http://localhost:8123" +CLICKHOUSE_USER = "" +CLICKHOUSE_PASSWORD = "" +CLICKHOUSE_DATABASE = "zed" # RUST_LOG=info # LOG_JSON=true diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 8beb83518b..214f8bf8aa 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -25,10 +25,12 @@ base64 = "0.13" chrono.workspace = true clap = { version = "3.1", features = ["derive"], optional = true } clock.workspace = true +clickhouse.workspace = true collections.workspace = true dashmap = "5.4" envy = "0.4.2" futures.workspace = true +hex.workspace = true hyper = "0.14" lazy_static.workspace = true lipsum = { version = "0.8", optional = true } @@ -48,8 +50,10 @@ serde.workspace = true serde_derive.workspace = true serde_json.workspace = true sha-1 = "0.9" +sha2.workspace = true smallvec.workspace = true sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] } +telemetry_events.workspace = true text.workspace = true time.workspace = true tokio = { version = "1", features = ["full"] } diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 44d6fc3eb5..d3e36a92e4 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -1,3 +1,4 @@ +pub mod events; mod extensions; use crate::{ diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs new file mode 100644 index 0000000000..52f69f86d9 --- /dev/null +++ b/crates/collab/src/api/events.rs @@ -0,0 +1,805 @@ +use std::sync::Arc; + +use anyhow::{anyhow, Context}; +use axum::{ + body::Bytes, headers::Header, http::HeaderName, routing::post, Extension, Router, TypedHeader, +}; +use hyper::StatusCode; +use lazy_static::lazy_static; +use serde::{Serialize, Serializer}; +use sha2::{Digest, Sha256}; +use telemetry_events::{ + ActionEvent, AppEvent, AssistantEvent, CallEvent, CopilotEvent, CpuEvent, EditEvent, + EditorEvent, Event, EventRequestBody, EventWrapper, MemoryEvent, SettingEvent, +}; + +use crate::{AppState, Error, Result}; + +pub fn router() -> Router { + Router::new().route("/telemetry/events", post(post_events)) +} + +lazy_static! { + static ref ZED_CHECKSUM_HEADER: HeaderName = HeaderName::from_static("x-zed-checksum"); + static ref CLOUDFLARE_IP_COUNTRY_HEADER: HeaderName = HeaderName::from_static("cf-ipcountry"); +} + +pub struct ZedChecksumHeader(Vec); + +impl Header for ZedChecksumHeader { + fn name() -> &'static HeaderName { + &ZED_CHECKSUM_HEADER + } + + fn decode<'i, I>(values: &mut I) -> Result + where + Self: Sized, + I: Iterator, + { + let checksum = values + .next() + .ok_or_else(axum::headers::Error::invalid)? + .to_str() + .map_err(|_| axum::headers::Error::invalid())?; + + let bytes = hex::decode(checksum).map_err(|_| axum::headers::Error::invalid())?; + Ok(Self(bytes)) + } + + fn encode>(&self, _values: &mut E) { + unimplemented!() + } +} + +pub struct CloudflareIpCountryHeader(String); + +impl Header for CloudflareIpCountryHeader { + fn name() -> &'static HeaderName { + &CLOUDFLARE_IP_COUNTRY_HEADER + } + + fn decode<'i, I>(values: &mut I) -> Result + where + Self: Sized, + I: Iterator, + { + let country_code = values + .next() + .ok_or_else(axum::headers::Error::invalid)? + .to_str() + .map_err(|_| axum::headers::Error::invalid())?; + + Ok(Self(country_code.to_string())) + } + + fn encode>(&self, _values: &mut E) { + unimplemented!() + } +} + +pub async fn post_events( + Extension(app): Extension>, + TypedHeader(ZedChecksumHeader(checksum)): TypedHeader, + country_code_header: Option>, + body: Bytes, +) -> Result<()> { + let Some(clickhouse_client) = app.clickhouse_client.clone() else { + Err(Error::Http( + StatusCode::NOT_IMPLEMENTED, + "not supported".into(), + ))? + }; + + let Some(checksum_seed) = app.config.zed_client_checksum_seed.as_ref() else { + return Err(Error::Http( + StatusCode::INTERNAL_SERVER_ERROR, + "events not enabled".into(), + ))?; + }; + + let mut summer = Sha256::new(); + summer.update(checksum_seed); + summer.update(&body); + summer.update(checksum_seed); + + if &checksum[..] != &summer.finalize()[..] { + return Err(Error::Http( + StatusCode::BAD_REQUEST, + "invalid checksum".into(), + ))?; + } + + let request_body: telemetry_events::EventRequestBody = + serde_json::from_slice(&body).map_err(|err| { + log::error!("can't parse event json: {err}"); + Error::Internal(anyhow!(err)) + })?; + + let mut to_upload = ToUpload::default(); + let Some(last_event) = request_body.events.last() else { + return Err(Error::Http(StatusCode::BAD_REQUEST, "no events".into()))?; + }; + let country_code = country_code_header.map(|h| h.0 .0); + + let first_event_at = chrono::Utc::now() + - chrono::Duration::milliseconds(last_event.milliseconds_since_first_event); + + for wrapper in &request_body.events { + match &wrapper.event { + Event::Editor(event) => to_upload.editor_events.push(EditorEventRow::from_event( + event.clone(), + &wrapper, + &request_body, + first_event_at, + country_code.clone(), + )), + Event::Copilot(event) => to_upload.copilot_events.push(CopilotEventRow::from_event( + event.clone(), + &wrapper, + &request_body, + first_event_at, + country_code.clone(), + )), + Event::Call(event) => to_upload.call_events.push(CallEventRow::from_event( + event.clone(), + &wrapper, + &request_body, + first_event_at, + )), + Event::Assistant(event) => { + to_upload + .assistant_events + .push(AssistantEventRow::from_event( + event.clone(), + &wrapper, + &request_body, + first_event_at, + )) + } + Event::Cpu(event) => to_upload.cpu_events.push(CpuEventRow::from_event( + event.clone(), + &wrapper, + &request_body, + first_event_at, + )), + Event::Memory(event) => to_upload.memory_events.push(MemoryEventRow::from_event( + event.clone(), + &wrapper, + &request_body, + first_event_at, + )), + Event::App(event) => to_upload.app_events.push(AppEventRow::from_event( + event.clone(), + &wrapper, + &request_body, + first_event_at, + )), + Event::Setting(event) => to_upload.setting_events.push(SettingEventRow::from_event( + event.clone(), + &wrapper, + &request_body, + first_event_at, + )), + Event::Edit(event) => to_upload.edit_events.push(EditEventRow::from_event( + event.clone(), + &wrapper, + &request_body, + first_event_at, + )), + Event::Action(event) => to_upload.action_events.push(ActionEventRow::from_event( + event.clone(), + &wrapper, + &request_body, + first_event_at, + )), + } + } + + to_upload + .upload(&clickhouse_client) + .await + .map_err(|err| Error::Internal(anyhow!(err)))?; + + Ok(()) +} + +#[derive(Default)] +struct ToUpload { + editor_events: Vec, + copilot_events: Vec, + assistant_events: Vec, + call_events: Vec, + cpu_events: Vec, + memory_events: Vec, + app_events: Vec, + setting_events: Vec, + edit_events: Vec, + action_events: Vec, +} + +impl ToUpload { + pub async fn upload(&self, clickhouse_client: &clickhouse::Client) -> anyhow::Result<()> { + Self::upload_to_table("editor_events", &self.editor_events, clickhouse_client) + .await + .with_context(|| format!("failed to upload to table 'editor_events'"))?; + Self::upload_to_table("copilot_events", &self.copilot_events, clickhouse_client) + .await + .with_context(|| format!("failed to upload to table 'copilot_events'"))?; + Self::upload_to_table( + "assistant_events", + &self.assistant_events, + clickhouse_client, + ) + .await + .with_context(|| format!("failed to upload to table 'assistant_events'"))?; + Self::upload_to_table("call_events", &self.call_events, clickhouse_client) + .await + .with_context(|| format!("failed to upload to table 'call_events'"))?; + Self::upload_to_table("cpu_events", &self.cpu_events, clickhouse_client) + .await + .with_context(|| format!("failed to upload to table 'cpu_events'"))?; + Self::upload_to_table("memory_events", &self.memory_events, clickhouse_client) + .await + .with_context(|| format!("failed to upload to table 'memory_events'"))?; + Self::upload_to_table("app_events", &self.app_events, clickhouse_client) + .await + .with_context(|| format!("failed to upload to table 'app_events'"))?; + Self::upload_to_table("setting_events", &self.setting_events, clickhouse_client) + .await + .with_context(|| format!("failed to upload to table 'setting_events'"))?; + Self::upload_to_table("edit_events", &self.edit_events, clickhouse_client) + .await + .with_context(|| format!("failed to upload to table 'edit_events'"))?; + Self::upload_to_table("action_events", &self.action_events, clickhouse_client) + .await + .with_context(|| format!("failed to upload to table 'action_events'"))?; + Ok(()) + } + + async fn upload_to_table( + table: &str, + rows: &[T], + clickhouse_client: &clickhouse::Client, + ) -> anyhow::Result<()> { + if !rows.is_empty() { + let mut insert = clickhouse_client.insert(table)?; + + for event in rows { + insert.write(event).await?; + } + + insert.end().await?; + } + + Ok(()) + } +} + +pub fn serialize_country_code(country_code: &str, serializer: S) -> Result +where + S: Serializer, +{ + if country_code.len() != 2 { + use serde::ser::Error; + return Err(S::Error::custom( + "country_code must be exactly 2 characters", + )); + } + + let country_code = country_code.as_bytes(); + + serializer.serialize_u16(((country_code[0] as u16) << 8) + country_code[1] as u16) +} + +#[derive(Serialize, Debug, clickhouse::Row)] +pub struct EditorEventRow { + pub installation_id: String, + pub operation: String, + pub app_version: String, + pub file_extension: String, + pub os_name: String, + pub os_version: String, + pub release_channel: String, + pub signed_in: bool, + pub vim_mode: bool, + #[serde(serialize_with = "serialize_country_code")] + pub country_code: String, + pub region_code: String, + pub city: String, + pub time: i64, + pub copilot_enabled: bool, + pub copilot_enabled_for_language: bool, + pub historical_event: bool, + pub architecture: String, + pub is_staff: Option, + pub session_id: Option, + pub major: Option, + pub minor: Option, + pub patch: Option, +} + +impl EditorEventRow { + fn from_event( + event: EditorEvent, + wrapper: &EventWrapper, + body: &EventRequestBody, + first_event_at: chrono::DateTime, + country_code: Option, + ) -> Self { + let semver = body.semver(); + let time = + first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event); + + Self { + app_version: body.app_version.clone(), + major: semver.map(|s| s.major as i32), + minor: semver.map(|s| s.minor as i32), + patch: semver.map(|s| s.patch as i32), + release_channel: body.release_channel.clone().unwrap_or_default(), + os_name: body.os_name.clone(), + os_version: body.os_version.clone().unwrap_or_default(), + architecture: body.architecture.clone(), + installation_id: body.installation_id.clone().unwrap_or_default(), + session_id: body.session_id.clone(), + is_staff: body.is_staff, + time: time.timestamp_millis(), + operation: event.operation, + file_extension: event.file_extension.unwrap_or_default(), + signed_in: wrapper.signed_in, + vim_mode: event.vim_mode, + copilot_enabled: event.copilot_enabled, + copilot_enabled_for_language: event.copilot_enabled_for_language, + country_code: country_code.unwrap_or("XX".to_string()), + region_code: "".to_string(), + city: "".to_string(), + historical_event: false, + } + } +} + +#[derive(Serialize, Debug, clickhouse::Row)] +pub struct CopilotEventRow { + pub installation_id: String, + pub suggestion_id: String, + pub suggestion_accepted: bool, + pub app_version: String, + pub file_extension: String, + pub os_name: String, + pub os_version: String, + pub release_channel: String, + pub signed_in: bool, + #[serde(serialize_with = "serialize_country_code")] + pub country_code: String, + pub region_code: String, + pub city: String, + pub time: i64, + pub is_staff: Option, + pub session_id: Option, + pub major: Option, + pub minor: Option, + pub patch: Option, +} + +impl CopilotEventRow { + fn from_event( + event: CopilotEvent, + wrapper: &EventWrapper, + body: &EventRequestBody, + first_event_at: chrono::DateTime, + country_code: Option, + ) -> Self { + let semver = body.semver(); + let time = + first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event); + + Self { + app_version: body.app_version.clone(), + major: semver.map(|s| s.major as i32), + minor: semver.map(|s| s.minor as i32), + patch: semver.map(|s| s.patch as i32), + release_channel: body.release_channel.clone().unwrap_or_default(), + os_name: body.os_name.clone(), + os_version: body.os_version.clone().unwrap_or_default(), + installation_id: body.installation_id.clone().unwrap_or_default(), + session_id: body.session_id.clone(), + is_staff: body.is_staff, + time: time.timestamp_millis(), + file_extension: event.file_extension.unwrap_or_default(), + signed_in: wrapper.signed_in, + country_code: country_code.unwrap_or("XX".to_string()), + region_code: "".to_string(), + city: "".to_string(), + suggestion_id: event.suggestion_id.unwrap_or_default(), + suggestion_accepted: event.suggestion_accepted, + } + } +} + +#[derive(Serialize, Debug, clickhouse::Row)] +pub struct CallEventRow { + // AppInfoBase + app_version: String, + major: Option, + minor: Option, + patch: Option, + release_channel: String, + + // ClientEventBase + installation_id: Option, + session_id: Option, + is_staff: Option, + time: i64, + + // CallEventRow + operation: String, + room_id: Option, + channel_id: Option, +} + +impl CallEventRow { + fn from_event( + event: CallEvent, + wrapper: &EventWrapper, + body: &EventRequestBody, + first_event_at: chrono::DateTime, + ) -> Self { + let semver = body.semver(); + let time = + first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event); + + Self { + app_version: body.app_version.clone(), + major: semver.map(|s| s.major as i32), + minor: semver.map(|s| s.minor as i32), + patch: semver.map(|s| s.patch as i32), + release_channel: body.release_channel.clone().unwrap_or_default(), + installation_id: body.installation_id.clone(), + session_id: body.session_id.clone(), + is_staff: body.is_staff, + time: time.timestamp_millis(), + operation: event.operation, + room_id: event.room_id, + channel_id: event.channel_id, + } + } +} + +#[derive(Serialize, Debug, clickhouse::Row)] +pub struct AssistantEventRow { + // AppInfoBase + app_version: String, + major: Option, + minor: Option, + patch: Option, + release_channel: String, + + // ClientEventBase + installation_id: Option, + session_id: Option, + is_staff: Option, + time: i64, + + // AssistantEventRow + conversation_id: Option, + kind: String, + model: String, +} + +impl AssistantEventRow { + fn from_event( + event: AssistantEvent, + wrapper: &EventWrapper, + body: &EventRequestBody, + first_event_at: chrono::DateTime, + ) -> Self { + let semver = body.semver(); + let time = + first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event); + + Self { + app_version: body.app_version.clone(), + major: semver.map(|s| s.major as i32), + minor: semver.map(|s| s.minor as i32), + patch: semver.map(|s| s.patch as i32), + release_channel: body.release_channel.clone().unwrap_or_default(), + installation_id: body.installation_id.clone(), + session_id: body.session_id.clone(), + is_staff: body.is_staff, + time: time.timestamp_millis(), + conversation_id: event.conversation_id, + kind: event.kind.to_string(), + model: event.model, + } + } +} + +#[derive(Debug, clickhouse::Row, Serialize)] +pub struct CpuEventRow { + pub installation_id: Option, + pub is_staff: Option, + pub usage_as_percentage: f32, + pub core_count: u32, + pub app_version: String, + pub release_channel: String, + pub time: i64, + pub session_id: Option, + // pub normalized_cpu_usage: f64, MATERIALIZED + pub major: Option, + pub minor: Option, + pub patch: Option, +} + +impl CpuEventRow { + fn from_event( + event: CpuEvent, + wrapper: &EventWrapper, + body: &EventRequestBody, + first_event_at: chrono::DateTime, + ) -> Self { + let semver = body.semver(); + let time = + first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event); + + Self { + app_version: body.app_version.clone(), + major: semver.map(|s| s.major as i32), + minor: semver.map(|s| s.minor as i32), + patch: semver.map(|s| s.patch as i32), + release_channel: body.release_channel.clone().unwrap_or_default(), + installation_id: body.installation_id.clone(), + session_id: body.session_id.clone(), + is_staff: body.is_staff, + time: time.timestamp_millis(), + usage_as_percentage: event.usage_as_percentage, + core_count: event.core_count, + } + } +} + +#[derive(Serialize, Debug, clickhouse::Row)] +pub struct MemoryEventRow { + // AppInfoBase + app_version: String, + major: Option, + minor: Option, + patch: Option, + release_channel: String, + + // ClientEventBase + installation_id: Option, + session_id: Option, + is_staff: Option, + time: i64, + + // MemoryEventRow + memory_in_bytes: u64, + virtual_memory_in_bytes: u64, +} + +impl MemoryEventRow { + fn from_event( + event: MemoryEvent, + wrapper: &EventWrapper, + body: &EventRequestBody, + first_event_at: chrono::DateTime, + ) -> Self { + let semver = body.semver(); + let time = + first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event); + + Self { + app_version: body.app_version.clone(), + major: semver.map(|s| s.major as i32), + minor: semver.map(|s| s.minor as i32), + patch: semver.map(|s| s.patch as i32), + release_channel: body.release_channel.clone().unwrap_or_default(), + installation_id: body.installation_id.clone(), + session_id: body.session_id.clone(), + is_staff: body.is_staff, + time: time.timestamp_millis(), + memory_in_bytes: event.memory_in_bytes, + virtual_memory_in_bytes: event.virtual_memory_in_bytes, + } + } +} + +#[derive(Serialize, Debug, clickhouse::Row)] +pub struct AppEventRow { + // AppInfoBase + app_version: String, + major: Option, + minor: Option, + patch: Option, + release_channel: String, + + // ClientEventBase + installation_id: Option, + session_id: Option, + is_staff: Option, + time: i64, + + // AppEventRow + operation: String, +} + +impl AppEventRow { + fn from_event( + event: AppEvent, + wrapper: &EventWrapper, + body: &EventRequestBody, + first_event_at: chrono::DateTime, + ) -> Self { + let semver = body.semver(); + let time = + first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event); + + Self { + app_version: body.app_version.clone(), + major: semver.map(|s| s.major as i32), + minor: semver.map(|s| s.minor as i32), + patch: semver.map(|s| s.patch as i32), + release_channel: body.release_channel.clone().unwrap_or_default(), + installation_id: body.installation_id.clone(), + session_id: body.session_id.clone(), + is_staff: body.is_staff, + time: time.timestamp_millis(), + operation: event.operation, + } + } +} + +#[derive(Serialize, Debug, clickhouse::Row)] +pub struct SettingEventRow { + // AppInfoBase + app_version: String, + major: Option, + minor: Option, + patch: Option, + release_channel: String, + + // ClientEventBase + installation_id: Option, + session_id: Option, + is_staff: Option, + time: i64, + // SettingEventRow + setting: String, + value: String, +} + +impl SettingEventRow { + fn from_event( + event: SettingEvent, + wrapper: &EventWrapper, + body: &EventRequestBody, + first_event_at: chrono::DateTime, + ) -> Self { + let semver = body.semver(); + let time = + first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event); + + Self { + app_version: body.app_version.clone(), + major: semver.map(|s| s.major as i32), + minor: semver.map(|s| s.minor as i32), + patch: semver.map(|s| s.patch as i32), + release_channel: body.release_channel.clone().unwrap_or_default(), + installation_id: body.installation_id.clone(), + session_id: body.session_id.clone(), + is_staff: body.is_staff, + time: time.timestamp_millis(), + setting: event.setting, + value: event.value, + } + } +} + +#[derive(Serialize, Debug, clickhouse::Row)] +pub struct EditEventRow { + // AppInfoBase + app_version: String, + major: Option, + minor: Option, + patch: Option, + release_channel: String, + + // SystemInfoBase + os_name: String, + os_version: Option, + architecture: String, + + // ClientEventBase + installation_id: Option, + // Note: This column name has a typo in the ClickHouse table. + #[serde(rename = "sesssion_id")] + session_id: Option, + is_staff: Option, + time: i64, + + // EditEventRow + period_start: i64, + period_end: i64, + environment: String, +} + +impl EditEventRow { + fn from_event( + event: EditEvent, + wrapper: &EventWrapper, + body: &EventRequestBody, + first_event_at: chrono::DateTime, + ) -> Self { + let semver = body.semver(); + let time = + first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event); + + let period_start = time - chrono::Duration::milliseconds(event.duration); + let period_end = time; + + Self { + app_version: body.app_version.clone(), + major: semver.map(|s| s.major as i32), + minor: semver.map(|s| s.minor as i32), + patch: semver.map(|s| s.patch as i32), + release_channel: body.release_channel.clone().unwrap_or_default(), + os_name: body.os_name.clone(), + os_version: body.os_version.clone(), + architecture: body.architecture.clone(), + installation_id: body.installation_id.clone(), + session_id: body.session_id.clone(), + is_staff: body.is_staff, + time: time.timestamp_millis(), + period_start: period_start.timestamp_millis(), + period_end: period_end.timestamp_millis(), + environment: event.environment, + } + } +} + +#[derive(Serialize, Debug, clickhouse::Row)] +pub struct ActionEventRow { + // AppInfoBase + app_version: String, + major: Option, + minor: Option, + patch: Option, + release_channel: String, + + // ClientEventBase + installation_id: Option, + // Note: This column name has a typo in the ClickHouse table. + #[serde(rename = "sesssion_id")] + session_id: Option, + is_staff: Option, + time: i64, + // ActionEventRow + source: String, + action: String, +} + +impl ActionEventRow { + fn from_event( + event: ActionEvent, + wrapper: &EventWrapper, + body: &EventRequestBody, + first_event_at: chrono::DateTime, + ) -> Self { + let semver = body.semver(); + let time = + first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event); + + Self { + app_version: body.app_version.clone(), + major: semver.map(|s| s.major as i32), + minor: semver.map(|s| s.minor as i32), + patch: semver.map(|s| s.patch as i32), + release_channel: body.release_channel.clone().unwrap_or_default(), + installation_id: body.installation_id.clone(), + session_id: body.session_id.clone(), + is_staff: body.is_staff, + time: time.timestamp_millis(), + source: event.source, + action: event.action, + } + } +} diff --git a/crates/collab/src/lib.rs b/crates/collab/src/lib.rs index 195ed7b11d..b3f31e86cc 100644 --- a/crates/collab/src/lib.rs +++ b/crates/collab/src/lib.rs @@ -58,11 +58,24 @@ impl From for Error { impl IntoResponse for Error { fn into_response(self) -> axum::response::Response { match self { - Error::Http(code, message) => (code, message).into_response(), + Error::Http(code, message) => { + log::error!("HTTP error {}: {}", code, &message); + (code, message).into_response() + } Error::Database(error) => { + log::error!( + "HTTP error {}: {}", + StatusCode::INTERNAL_SERVER_ERROR, + &error + ); (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response() } Error::Internal(error) => { + log::error!( + "HTTP error {}: {}", + StatusCode::INTERNAL_SERVER_ERROR, + &error + ); (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response() } } @@ -97,6 +110,10 @@ pub struct Config { pub database_url: String, pub database_max_connections: u32, pub api_token: String, + pub clickhouse_url: Option, + pub clickhouse_user: Option, + pub clickhouse_password: Option, + pub clickhouse_database: Option, pub invite_link_prefix: String, pub live_kit_server: Option, pub live_kit_key: Option, @@ -109,6 +126,7 @@ pub struct Config { pub blob_store_secret_key: Option, pub blob_store_bucket: Option, pub zed_environment: Arc, + pub zed_client_checksum_seed: Option, } impl Config { @@ -127,6 +145,7 @@ pub struct AppState { pub db: Arc, pub live_kit_client: Option>, pub blob_store_client: Option, + pub clickhouse_client: Option, pub config: Config, } @@ -156,6 +175,7 @@ impl AppState { db: Arc::new(db), live_kit_client, blob_store_client: build_blob_store_client(&config).await.log_err(), + clickhouse_client: build_clickhouse_client(&config).log_err(), config, }; Ok(Arc::new(this)) @@ -196,3 +216,31 @@ async fn build_blob_store_client(config: &Config) -> anyhow::Result anyhow::Result { + Ok(clickhouse::Client::default() + .with_url( + config + .clickhouse_url + .as_ref() + .ok_or_else(|| anyhow!("missing clickhouse_url"))?, + ) + .with_user( + config + .clickhouse_user + .as_ref() + .ok_or_else(|| anyhow!("missing clickhouse_user"))?, + ) + .with_password( + config + .clickhouse_password + .as_ref() + .ok_or_else(|| anyhow!("missing clickhouse_password"))?, + ) + .with_database( + config + .clickhouse_database + .as_ref() + .ok_or_else(|| anyhow!("missing clickhouse_database"))?, + )) +} diff --git a/crates/collab/src/main.rs b/crates/collab/src/main.rs index b80e8961df..b3e93bf94d 100644 --- a/crates/collab/src/main.rs +++ b/crates/collab/src/main.rs @@ -61,6 +61,7 @@ async fn main() -> Result<()> { Router::new() .route("/", get(handle_root)) .route("/healthz", get(handle_liveness_probe)) + .merge(collab::api::events::router()) .layer(Extension(state.clone())), ); diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 56705f182d..f79698de48 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -483,6 +483,7 @@ impl TestServer { db: test_db.db().clone(), live_kit_client: Some(Arc::new(fake_server.create_api_client())), blob_store_client: None, + clickhouse_client: None, config: Config { http_port: 0, database_url: "".into(), @@ -500,6 +501,11 @@ impl TestServer { blob_store_access_key: None, blob_store_secret_key: None, blob_store_bucket: None, + clickhouse_url: None, + clickhouse_user: None, + clickhouse_password: None, + clickhouse_database: None, + zed_client_checksum_seed: None, }, }) } diff --git a/crates/telemetry_events/Cargo.toml b/crates/telemetry_events/Cargo.toml new file mode 100644 index 0000000000..6893e7c183 --- /dev/null +++ b/crates/telemetry_events/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "telemetry_events" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lib] +path = "src/telemetry_events.rs" + +[dependencies] +serde.workspace = true +util.workspace = true diff --git a/crates/telemetry_events/LICENSE-GPL b/crates/telemetry_events/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/telemetry_events/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/telemetry_events/src/telemetry_events.rs b/crates/telemetry_events/src/telemetry_events.rs new file mode 100644 index 0000000000..d2ea0610db --- /dev/null +++ b/crates/telemetry_events/src/telemetry_events.rs @@ -0,0 +1,131 @@ +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; +use util::SemanticVersion; + +#[derive(Serialize, Deserialize, Debug)] +pub struct EventRequestBody { + pub installation_id: Option, + pub session_id: Option, + pub is_staff: Option, + pub app_version: String, + pub os_name: String, + pub os_version: Option, + pub architecture: String, + pub release_channel: Option, + pub events: Vec, +} + +impl EventRequestBody { + pub fn semver(&self) -> Option { + self.app_version.parse().ok() + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct EventWrapper { + pub signed_in: bool, + pub milliseconds_since_first_event: i64, + #[serde(flatten)] + pub event: Event, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AssistantKind { + Panel, + Inline, +} + +impl Display for AssistantKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Panel => "panel", + Self::Inline => "inline", + } + ) + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum Event { + Editor(EditorEvent), + Copilot(CopilotEvent), + Call(CallEvent), + Assistant(AssistantEvent), + Cpu(CpuEvent), + Memory(MemoryEvent), + App(AppEvent), + Setting(SettingEvent), + Edit(EditEvent), + Action(ActionEvent), +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct EditorEvent { + pub operation: String, + pub file_extension: Option, + pub vim_mode: bool, + pub copilot_enabled: bool, + pub copilot_enabled_for_language: bool, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct CopilotEvent { + pub suggestion_id: Option, + pub suggestion_accepted: bool, + pub file_extension: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct CallEvent { + pub operation: String, + pub room_id: Option, + pub channel_id: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct AssistantEvent { + pub conversation_id: Option, + pub kind: AssistantKind, + pub model: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct CpuEvent { + pub usage_as_percentage: f32, + pub core_count: u32, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MemoryEvent { + pub memory_in_bytes: u64, + pub virtual_memory_in_bytes: u64, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct ActionEvent { + pub source: String, + pub action: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct EditEvent { + pub duration: i64, + pub environment: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct SettingEvent { + pub setting: String, + pub value: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct AppEvent { + pub operation: String, +} diff --git a/crates/util/src/http.rs b/crates/util/src/http.rs index aff5093b73..89b1b482e9 100644 --- a/crates/util/src/http.rs +++ b/crates/util/src/http.rs @@ -23,6 +23,19 @@ impl ZedHttpClient { pub fn zed_url(&self, path: &str) -> String { format!("{}{}", self.zed_host.lock(), path) } + + pub fn zed_api_url(&self, path: &str) -> String { + let zed_host = self.zed_host.lock().clone(); + + let host = match zed_host.as_ref() { + "https://zed.dev" => "https://api.zed.dev", + "https://staging.zed.dev" => "https://api-staging.zed.dev", + "http://localhost:3000" => "http://localhost:8080", + other => other, + }; + + format!("{}{}", host, path) + } } impl HttpClient for Arc { diff --git a/typos.toml b/typos.toml index 0923d130c9..92becd95bf 100644 --- a/typos.toml +++ b/typos.toml @@ -22,5 +22,8 @@ extend-ignore-re = [ ":ba\\|z", # :/ crates/collab/migrations/20231009181554_add_release_channel_to_rooms.sql "COLUMN enviroment", + # Typo in ClickHouse column name. + # crates/collab/src/api/events.rs + "rename = \"sesssion_id\"" ] check-filename = true