diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 46304819a4..6c1803df3d 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -37,9 +37,10 @@ pub struct Telemetry { struct TelemetryState { settings: TelemetrySettings, - metrics_id: Option>, // Per logged-in user + system_id: Option>, // Per system installation_id: Option>, // Per app installation (different for dev, nightly, preview, and stable) session_id: Option, // Per app launch + metrics_id: Option>, // Per logged-in user release_channel: Option<&'static str>, architecture: &'static str, events_queue: Vec, @@ -191,9 +192,10 @@ impl Telemetry { settings: *TelemetrySettings::get_global(cx), architecture: env::consts::ARCH, release_channel, + system_id: None, installation_id: None, - metrics_id: None, session_id: None, + metrics_id: None, events_queue: Vec::new(), flush_events_task: None, log_file: None, @@ -283,11 +285,13 @@ impl Telemetry { pub fn start( self: &Arc, + system_id: Option, installation_id: Option, session_id: String, cx: &mut AppContext, ) { let mut state = self.state.lock(); + state.system_id = system_id.map(|id| id.into()); state.installation_id = installation_id.map(|id| id.into()); state.session_id = Some(session_id); state.app_version = release_channel::AppVersion::global(cx).to_string(); @@ -637,9 +641,10 @@ impl Telemetry { let state = this.state.lock(); let request_body = EventRequestBody { + system_id: state.system_id.as_deref().map(Into::into), installation_id: state.installation_id.as_deref().map(Into::into), - metrics_id: state.metrics_id.as_deref().map(Into::into), session_id: state.session_id.clone(), + metrics_id: state.metrics_id.as_deref().map(Into::into), is_staff: state.is_staff, app_version: state.app_version.clone(), os_name: state.os_name.clone(), @@ -711,6 +716,7 @@ mod tests { Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(), )); let http = FakeHttpClient::with_200_response(); + let system_id = Some("system_id".to_string()); let installation_id = Some("installation_id".to_string()); let session_id = "session_id".to_string(); @@ -718,7 +724,7 @@ mod tests { let telemetry = Telemetry::new(clock.clone(), http, cx); telemetry.state.lock().max_queue_size = 4; - telemetry.start(installation_id, session_id, cx); + telemetry.start(system_id, installation_id, session_id, cx); assert!(is_empty_state(&telemetry)); @@ -796,13 +802,14 @@ mod tests { Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(), )); let http = FakeHttpClient::with_200_response(); + let system_id = Some("system_id".to_string()); let installation_id = Some("installation_id".to_string()); let session_id = "session_id".to_string(); cx.update(|cx| { let telemetry = Telemetry::new(clock.clone(), http, cx); telemetry.state.lock().max_queue_size = 4; - telemetry.start(installation_id, session_id, cx); + telemetry.start(system_id, installation_id, session_id, cx); assert!(is_empty_state(&telemetry)); diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index 45c25d261e..1be8f9c37b 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -149,7 +149,8 @@ pub async fn post_crash( installation_id = %installation_id, description = %description, backtrace = %summary, - "crash report"); + "crash report" + ); if let Some(slack_panics_webhook) = app.config.slack_panics_webhook.clone() { let payload = slack::WebhookBody::new(|w| { @@ -627,7 +628,9 @@ where #[derive(Serialize, Debug, clickhouse::Row)] pub struct EditorEventRow { + system_id: String, installation_id: String, + session_id: Option, metrics_id: String, operation: String, app_version: String, @@ -647,7 +650,6 @@ pub struct EditorEventRow { historical_event: bool, architecture: String, is_staff: Option, - session_id: Option, major: Option, minor: Option, patch: Option, @@ -677,9 +679,10 @@ impl EditorEventRow { os_name: body.os_name.clone(), os_version: body.os_version.clone().unwrap_or_default(), architecture: body.architecture.clone(), + system_id: body.system_id.clone().unwrap_or_default(), installation_id: body.installation_id.clone().unwrap_or_default(), - metrics_id: body.metrics_id.clone().unwrap_or_default(), session_id: body.session_id.clone(), + metrics_id: body.metrics_id.clone().unwrap_or_default(), is_staff: body.is_staff, time: time.timestamp_millis(), operation: event.operation, @@ -699,6 +702,7 @@ impl EditorEventRow { #[derive(Serialize, Debug, clickhouse::Row)] pub struct InlineCompletionEventRow { installation_id: String, + session_id: Option, provider: String, suggestion_accepted: bool, app_version: String, @@ -713,7 +717,6 @@ pub struct InlineCompletionEventRow { city: String, time: i64, is_staff: Option, - session_id: Option, major: Option, minor: Option, patch: Option, @@ -879,7 +882,9 @@ impl AssistantEventRow { #[derive(Debug, clickhouse::Row, Serialize)] pub struct CpuEventRow { + system_id: Option, installation_id: Option, + session_id: Option, is_staff: Option, usage_as_percentage: f32, core_count: u32, @@ -888,7 +893,6 @@ pub struct CpuEventRow { os_name: String, os_version: String, time: i64, - session_id: Option, // pub normalized_cpu_usage: f64, MATERIALIZED major: Option, minor: Option, @@ -917,6 +921,7 @@ impl CpuEventRow { release_channel: body.release_channel.clone().unwrap_or_default(), os_name: body.os_name.clone(), os_version: body.os_version.clone().unwrap_or_default(), + system_id: body.system_id.clone(), installation_id: body.installation_id.clone(), session_id: body.session_id.clone(), is_staff: body.is_staff, @@ -940,6 +945,7 @@ pub struct MemoryEventRow { os_version: String, // ClientEventBase + system_id: Option, installation_id: Option, session_id: Option, is_staff: Option, @@ -971,6 +977,7 @@ impl MemoryEventRow { release_channel: body.release_channel.clone().unwrap_or_default(), os_name: body.os_name.clone(), os_version: body.os_version.clone().unwrap_or_default(), + system_id: body.system_id.clone(), installation_id: body.installation_id.clone(), session_id: body.session_id.clone(), is_staff: body.is_staff, @@ -994,6 +1001,7 @@ pub struct AppEventRow { os_version: String, // ClientEventBase + system_id: Option, installation_id: Option, session_id: Option, is_staff: Option, @@ -1024,6 +1032,7 @@ impl AppEventRow { release_channel: body.release_channel.clone().unwrap_or_default(), os_name: body.os_name.clone(), os_version: body.os_version.clone().unwrap_or_default(), + system_id: body.system_id.clone(), installation_id: body.installation_id.clone(), session_id: body.session_id.clone(), is_staff: body.is_staff, @@ -1046,6 +1055,7 @@ pub struct SettingEventRow { os_version: String, // ClientEventBase + system_id: Option, installation_id: Option, session_id: Option, is_staff: Option, @@ -1076,6 +1086,7 @@ impl SettingEventRow { release_channel: body.release_channel.clone().unwrap_or_default(), os_name: body.os_name.clone(), os_version: body.os_version.clone().unwrap_or_default(), + system_id: body.system_id.clone(), installation_id: body.installation_id.clone(), session_id: body.session_id.clone(), is_staff: body.is_staff, @@ -1099,6 +1110,7 @@ pub struct ExtensionEventRow { os_version: String, // ClientEventBase + system_id: Option, installation_id: Option, session_id: Option, is_staff: Option, @@ -1134,6 +1146,7 @@ impl ExtensionEventRow { release_channel: body.release_channel.clone().unwrap_or_default(), os_name: body.os_name.clone(), os_version: body.os_version.clone().unwrap_or_default(), + system_id: body.system_id.clone(), installation_id: body.installation_id.clone(), session_id: body.session_id.clone(), is_staff: body.is_staff, @@ -1224,6 +1237,7 @@ pub struct EditEventRow { os_version: String, // ClientEventBase + system_id: Option, installation_id: Option, // Note: This column name has a typo in the ClickHouse table. #[serde(rename = "sesssion_id")] @@ -1261,6 +1275,7 @@ impl EditEventRow { release_channel: body.release_channel.clone().unwrap_or_default(), os_name: body.os_name.clone(), os_version: body.os_version.clone().unwrap_or_default(), + system_id: body.system_id.clone(), installation_id: body.installation_id.clone(), session_id: body.session_id.clone(), is_staff: body.is_staff, diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index 768f382203..4d87222c77 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -11,16 +11,14 @@ pub use smol; pub use sqlez; pub use sqlez_macros; -use release_channel::ReleaseChannel; pub use release_channel::RELEASE_CHANNEL; use sqlez::domain::Migrator; use sqlez::thread_safe_connection::ThreadSafeConnection; use sqlez_macros::sql; -use std::env; use std::future::Future; use std::path::Path; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::LazyLock; +use std::sync::{atomic::Ordering, LazyLock}; +use std::{env, sync::atomic::AtomicBool}; use util::{maybe, ResultExt}; const CONNECTION_INITIALIZE_QUERY: &str = sql!( @@ -47,16 +45,12 @@ pub static ALL_FILE_DB_FAILED: LazyLock = LazyLock::new(|| AtomicBoo /// This will retry a couple times if there are failures. If opening fails once, the db directory /// is moved to a backup folder and a new one is created. If that fails, a shared in memory db is created. /// In either case, static variables are set so that the user can be notified. -pub async fn open_db( - db_dir: &Path, - release_channel: &ReleaseChannel, -) -> ThreadSafeConnection { +pub async fn open_db(db_dir: &Path, scope: &str) -> ThreadSafeConnection { if *ZED_STATELESS { return open_fallback_db().await; } - let release_channel_name = release_channel.dev_name(); - let main_db_dir = db_dir.join(Path::new(&format!("0-{}", release_channel_name))); + let main_db_dir = db_dir.join(format!("0-{}", scope)); let connection = maybe!(async { smol::fs::create_dir_all(&main_db_dir) @@ -118,7 +112,7 @@ pub async fn open_test_db(db_name: &str) -> ThreadSafeConnection /// Implements a basic DB wrapper for a given domain #[macro_export] macro_rules! define_connection { - (pub static ref $id:ident: $t:ident<()> = $migrations:expr;) => { + (pub static ref $id:ident: $t:ident<()> = $migrations:expr; $($global:ident)?) => { pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection<$t>); impl ::std::ops::Deref for $t { @@ -139,18 +133,23 @@ macro_rules! define_connection { } } - use std::sync::LazyLock; #[cfg(any(test, feature = "test-support"))] - pub static $id: LazyLock<$t> = LazyLock::new(|| { + pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { $t($crate::smol::block_on($crate::open_test_db(stringify!($id)))) }); #[cfg(not(any(test, feature = "test-support")))] - pub static $id: LazyLock<$t> = LazyLock::new(|| { - $t($crate::smol::block_on($crate::open_db($crate::database_dir(), &$crate::RELEASE_CHANNEL))) + pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { + let db_dir = $crate::database_dir(); + let scope = if false $(|| stringify!($global) == "global")? { + "global" + } else { + $crate::RELEASE_CHANNEL.dev_name() + }; + $t($crate::smol::block_on($crate::open_db(db_dir, scope))) }); }; - (pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr;) => { + (pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr; $($global:ident)?) => { pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection<( $($d),+, $t )>); impl ::std::ops::Deref for $t { @@ -178,7 +177,13 @@ macro_rules! define_connection { #[cfg(not(any(test, feature = "test-support")))] pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { - $t($crate::smol::block_on($crate::open_db($crate::database_dir(), &$crate::RELEASE_CHANNEL))) + let db_dir = $crate::database_dir(); + let scope = if false $(|| stringify!($global) == "global")? { + "global" + } else { + $crate::RELEASE_CHANNEL.dev_name() + }; + $t($crate::smol::block_on($crate::open_db(db_dir, scope))) }); }; } @@ -225,7 +230,11 @@ mod tests { .prefix("DbTests") .tempdir() .unwrap(); - let _bad_db = open_db::(tempdir.path(), &release_channel::ReleaseChannel::Dev).await; + let _bad_db = open_db::( + tempdir.path(), + &release_channel::ReleaseChannel::Dev.dev_name(), + ) + .await; } /// Test that DB exists but corrupted (causing recreate) @@ -262,13 +271,19 @@ mod tests { .tempdir() .unwrap(); { - let corrupt_db = - open_db::(tempdir.path(), &release_channel::ReleaseChannel::Dev).await; + let corrupt_db = open_db::( + tempdir.path(), + &release_channel::ReleaseChannel::Dev.dev_name(), + ) + .await; assert!(corrupt_db.persistent()); } - let good_db = - open_db::(tempdir.path(), &release_channel::ReleaseChannel::Dev).await; + let good_db = open_db::( + tempdir.path(), + &release_channel::ReleaseChannel::Dev.dev_name(), + ) + .await; assert!( good_db.select_row::("SELECT * FROM test2").unwrap()() .unwrap() @@ -311,8 +326,11 @@ mod tests { .unwrap(); { // Setup the bad database - let corrupt_db = - open_db::(tempdir.path(), &release_channel::ReleaseChannel::Dev).await; + let corrupt_db = open_db::( + tempdir.path(), + &release_channel::ReleaseChannel::Dev.dev_name(), + ) + .await; assert!(corrupt_db.persistent()); } @@ -323,7 +341,7 @@ mod tests { let guard = thread::spawn(move || { let good_db = smol::block_on(open_db::( tmp_path.as_path(), - &release_channel::ReleaseChannel::Dev, + &release_channel::ReleaseChannel::Dev.dev_name(), )); assert!( good_db.select_row::("SELECT * FROM test2").unwrap()() diff --git a/crates/db/src/kvp.rs b/crates/db/src/kvp.rs index 0b0cdd9aa1..c9d994d34d 100644 --- a/crates/db/src/kvp.rs +++ b/crates/db/src/kvp.rs @@ -60,3 +60,33 @@ mod tests { assert_eq!(db.read_kvp("key-1").unwrap(), None); } } + +define_connection!(pub static ref GLOBAL_KEY_VALUE_STORE: GlobalKeyValueStore<()> = + &[sql!( + CREATE TABLE IF NOT EXISTS kv_store( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) STRICT; + )]; + global +); + +impl GlobalKeyValueStore { + query! { + pub fn read_kvp(key: &str) -> Result> { + SELECT value FROM kv_store WHERE key = (?) + } + } + + query! { + pub async fn write_kvp(key: String, value: String) -> Result<()> { + INSERT OR REPLACE INTO kv_store(key, value) VALUES ((?), (?)) + } + } + + query! { + pub async fn delete_kvp(key: String) -> Result<()> { + DELETE FROM kv_store WHERE key = (?) + } + } +} diff --git a/crates/feedback/src/feedback_modal.rs b/crates/feedback/src/feedback_modal.rs index 7369bcd853..a4a07ad2ad 100644 --- a/crates/feedback/src/feedback_modal.rs +++ b/crates/feedback/src/feedback_modal.rs @@ -44,8 +44,8 @@ const FEEDBACK_SUBMISSION_ERROR_TEXT: &str = struct FeedbackRequestBody<'a> { feedback_text: &'a str, email: Option, - metrics_id: Option>, installation_id: Option>, + metrics_id: Option>, system_specs: SystemSpecs, is_staff: bool, } @@ -296,16 +296,16 @@ impl FeedbackModal { } let telemetry = zed_client.telemetry(); - let metrics_id = telemetry.metrics_id(); let installation_id = telemetry.installation_id(); + let metrics_id = telemetry.metrics_id(); let is_staff = telemetry.is_staff(); let http_client = zed_client.http_client(); let feedback_endpoint = http_client.build_url("/api/feedback"); let request = FeedbackRequestBody { feedback_text, email, - metrics_id, installation_id, + metrics_id, system_specs, is_staff: is_staff.unwrap_or(false), }; diff --git a/crates/telemetry_events/src/telemetry_events.rs b/crates/telemetry_events/src/telemetry_events.rs index eb84322e83..d6e737b929 100644 --- a/crates/telemetry_events/src/telemetry_events.rs +++ b/crates/telemetry_events/src/telemetry_events.rs @@ -5,12 +5,14 @@ use std::{fmt::Display, sync::Arc, time::Duration}; #[derive(Serialize, Deserialize, Debug)] pub struct EventRequestBody { + /// Identifier unique to each system Zed is installed on + pub system_id: Option, /// Identifier unique to each Zed installation (differs for stable, preview, dev) pub installation_id: Option, /// Identifier unique to each logged in Zed user (randomly generated on first sign in) - pub metrics_id: Option, /// Identifier unique to each Zed session (differs for each time you open Zed) pub session_id: Option, + pub metrics_id: Option, /// True for Zed staff, otherwise false pub is_staff: Option, /// Zed version number @@ -34,6 +36,7 @@ pub struct EventWrapper { pub signed_in: bool, /// Duration between this event's timestamp and the timestamp of the first event in the current batch pub milliseconds_since_first_event: i64, + /// The event itself #[serde(flatten)] pub event: Event, } @@ -245,8 +248,11 @@ pub struct Panic { pub architecture: String, /// The time the panic occurred (UNIX millisecond timestamp) pub panicked_on: i64, + /// Identifier unique to each system Zed is installed on #[serde(skip_serializing_if = "Option::is_none")] + pub system_id: Option, /// Identifier unique to each Zed installation (differs for stable, preview, dev) + #[serde(skip_serializing_if = "Option::is_none")] pub installation_id: Option, /// Identifier unique to each Zed session (differs for each time you open Zed) pub session_id: String, diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index d3a722ec65..c127a975a9 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -13,7 +13,7 @@ use clap::{command, Parser}; use cli::FORCE_CLI_MODE_ENV_VAR_NAME; use client::{parse_zed_link, Client, DevServerToken, ProxySettings, UserStore}; use collab_ui::channel_view::ChannelView; -use db::kvp::KEY_VALUE_STORE; +use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE}; use editor::Editor; use env_logger::Builder; use fs::{Fs, RealFs}; @@ -334,19 +334,17 @@ fn main() { .with_assets(Assets) .with_http_client(IsahcHttpClient::new(None, None)); - let (installation_id, existing_installation_id_found) = app - .background_executor() - .block(installation_id()) - .ok() - .unzip(); - + let system_id = app.background_executor().block(system_id()).ok(); + let installation_id = app.background_executor().block(installation_id()).ok(); + let session_id = Uuid::new_v4().to_string(); let session = app.background_executor().block(Session::new()); - let app_version = AppVersion::init(env!("CARGO_PKG_VERSION")); + reliability::init_panic_hook( - installation_id.clone(), app_version, - session.id().to_owned(), + system_id.as_ref().map(|id| id.to_string()), + installation_id.as_ref().map(|id| id.to_string()), + session_id.clone(), ); let (open_listener, mut open_rx) = OpenListener::new(); @@ -491,14 +489,26 @@ fn main() { client::init(&client, cx); language::init(cx); let telemetry = client.telemetry(); - telemetry.start(installation_id.clone(), session.id().to_owned(), cx); - telemetry.report_app_event( - match existing_installation_id_found { - Some(false) => "first open", - _ => "open", - } - .to_string(), + telemetry.start( + system_id.as_ref().map(|id| id.to_string()), + installation_id.as_ref().map(|id| id.to_string()), + session_id, + cx, ); + if let (Some(system_id), Some(installation_id)) = (&system_id, &installation_id) { + match (&system_id, &installation_id) { + (IdType::New(_), IdType::New(_)) => { + telemetry.report_app_event("first open".to_string()); + telemetry.report_app_event("first open for release channel".to_string()); + } + (IdType::Existing(_), IdType::New(_)) => { + telemetry.report_app_event("first open for release channel".to_string()); + } + (_, IdType::Existing(_)) => { + telemetry.report_app_event("open".to_string()); + } + } + } let app_session = cx.new_model(|cx| AppSession::new(session, cx)); let app_state = Arc::new(AppState { @@ -514,7 +524,11 @@ fn main() { AppState::set_global(Arc::downgrade(&app_state), cx); auto_update::init(client.http_client(), cx); - reliability::init(client.http_client(), installation_id, cx); + reliability::init( + client.http_client(), + installation_id.clone().map(|id| id.to_string()), + cx, + ); let prompt_builder = init_common(app_state.clone(), cx); let args = Args::parse(); @@ -755,7 +769,23 @@ async fn authenticate(client: Arc, cx: &AsyncAppContext) -> Result<()> { Ok::<_, anyhow::Error>(()) } -async fn installation_id() -> Result<(String, bool)> { +async fn system_id() -> Result { + let key_name = "system_id".to_string(); + + if let Ok(Some(system_id)) = GLOBAL_KEY_VALUE_STORE.read_kvp(&key_name) { + return Ok(IdType::Existing(system_id)); + } + + let system_id = Uuid::new_v4().to_string(); + + GLOBAL_KEY_VALUE_STORE + .write_kvp(key_name, system_id.clone()) + .await?; + + Ok(IdType::New(system_id)) +} + +async fn installation_id() -> Result { let legacy_key_name = "device_id".to_string(); let key_name = "installation_id".to_string(); @@ -765,11 +795,11 @@ async fn installation_id() -> Result<(String, bool)> { .write_kvp(key_name, installation_id.clone()) .await?; KEY_VALUE_STORE.delete_kvp(legacy_key_name).await?; - return Ok((installation_id, true)); + return Ok(IdType::Existing(installation_id)); } if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp(&key_name) { - return Ok((installation_id, true)); + return Ok(IdType::Existing(installation_id)); } let installation_id = Uuid::new_v4().to_string(); @@ -778,7 +808,7 @@ async fn installation_id() -> Result<(String, bool)> { .write_kvp(key_name, installation_id.clone()) .await?; - Ok((installation_id, false)) + Ok(IdType::New(installation_id)) } async fn restore_or_create_workspace( @@ -1087,6 +1117,20 @@ struct Args { dev_server_token: Option, } +#[derive(Clone, Debug)] +enum IdType { + New(String), + Existing(String), +} + +impl ToString for IdType { + fn to_string(&self) -> String { + match self { + IdType::New(id) | IdType::Existing(id) => id.clone(), + } + } +} + fn parse_url_arg(arg: &str, cx: &AppContext) -> Result { match std::fs::canonicalize(Path::new(&arg)) { Ok(path) => Ok(format!( diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index 188cf417f7..9e811d7c9a 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -28,8 +28,9 @@ use crate::stdout_is_a_pty; static PANIC_COUNT: AtomicU32 = AtomicU32::new(0); pub fn init_panic_hook( - installation_id: Option, app_version: SemanticVersion, + system_id: Option, + installation_id: Option, session_id: String, ) { let is_pty = stdout_is_a_pty(); @@ -102,6 +103,7 @@ pub fn init_panic_hook( architecture: env::consts::ARCH.into(), panicked_on: Utc::now().timestamp_millis(), backtrace, + system_id: system_id.clone(), installation_id: installation_id.clone(), session_id: session_id.clone(), };