Add system_id (#18040)

Joseph T. Lyons created

This PR adds `system_id` to telemetry, which is contained within a new
`global` database (accessible by any release channel of Zed on a single
system). This will help us get a more accurate understanding of user
count, instead of relying on `installationd_id`, which is different per
release channel. This doesn't solve the problem of a user with multiple
machines, but it gets us closer.

Release Notes:

- N/A

Change summary

crates/client/src/telemetry.rs                  | 17 ++-
crates/collab/src/api/events.rs                 | 25 ++++-
crates/db/src/db.rs                             | 68 +++++++++-----
crates/db/src/kvp.rs                            | 30 ++++++
crates/feedback/src/feedback_modal.rs           |  6 
crates/telemetry_events/src/telemetry_events.rs |  8 +
crates/zed/src/main.rs                          | 88 ++++++++++++++----
crates/zed/src/reliability.rs                   |  4 
8 files changed, 184 insertions(+), 62 deletions(-)

Detailed changes

crates/client/src/telemetry.rs 🔗

@@ -37,9 +37,10 @@ pub struct Telemetry {
 
 struct TelemetryState {
     settings: TelemetrySettings,
-    metrics_id: Option<Arc<str>>,      // Per logged-in user
+    system_id: Option<Arc<str>>,       // Per system
     installation_id: Option<Arc<str>>, // Per app installation (different for dev, nightly, preview, and stable)
     session_id: Option<String>,        // Per app launch
+    metrics_id: Option<Arc<str>>,      // Per logged-in user
     release_channel: Option<&'static str>,
     architecture: &'static str,
     events_queue: Vec<EventWrapper>,
@@ -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<Self>,
+        system_id: Option<String>,
         installation_id: Option<String>,
         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));
 

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<String>,
     metrics_id: String,
     operation: String,
     app_version: String,
@@ -647,7 +650,6 @@ pub struct EditorEventRow {
     historical_event: bool,
     architecture: String,
     is_staff: Option<bool>,
-    session_id: Option<String>,
     major: Option<i32>,
     minor: Option<i32>,
     patch: Option<i32>,
@@ -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<String>,
     provider: String,
     suggestion_accepted: bool,
     app_version: String,
@@ -713,7 +717,6 @@ pub struct InlineCompletionEventRow {
     city: String,
     time: i64,
     is_staff: Option<bool>,
-    session_id: Option<String>,
     major: Option<i32>,
     minor: Option<i32>,
     patch: Option<i32>,
@@ -879,7 +882,9 @@ impl AssistantEventRow {
 
 #[derive(Debug, clickhouse::Row, Serialize)]
 pub struct CpuEventRow {
+    system_id: Option<String>,
     installation_id: Option<String>,
+    session_id: Option<String>,
     is_staff: Option<bool>,
     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<String>,
     // pub normalized_cpu_usage: f64, MATERIALIZED
     major: Option<i32>,
     minor: Option<i32>,
@@ -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<String>,
     installation_id: Option<String>,
     session_id: Option<String>,
     is_staff: Option<bool>,
@@ -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<String>,
     installation_id: Option<String>,
     session_id: Option<String>,
     is_staff: Option<bool>,
@@ -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<String>,
     installation_id: Option<String>,
     session_id: Option<String>,
     is_staff: Option<bool>,
@@ -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<String>,
     installation_id: Option<String>,
     session_id: Option<String>,
     is_staff: Option<bool>,
@@ -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<String>,
     installation_id: Option<String>,
     // 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,

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<AtomicBool> = 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<M: Migrator + 'static>(
-    db_dir: &Path,
-    release_channel: &ReleaseChannel,
-) -> ThreadSafeConnection<M> {
+pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, scope: &str) -> ThreadSafeConnection<M> {
     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<M: Migrator>(db_name: &str) -> ThreadSafeConnection<M>
 /// 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::<BadDB>(tempdir.path(), &release_channel::ReleaseChannel::Dev).await;
+        let _bad_db = open_db::<BadDB>(
+            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::<CorruptedDB>(tempdir.path(), &release_channel::ReleaseChannel::Dev).await;
+            let corrupt_db = open_db::<CorruptedDB>(
+                tempdir.path(),
+                &release_channel::ReleaseChannel::Dev.dev_name(),
+            )
+            .await;
             assert!(corrupt_db.persistent());
         }
 
-        let good_db =
-            open_db::<GoodDB>(tempdir.path(), &release_channel::ReleaseChannel::Dev).await;
+        let good_db = open_db::<GoodDB>(
+            tempdir.path(),
+            &release_channel::ReleaseChannel::Dev.dev_name(),
+        )
+        .await;
         assert!(
             good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
                 .unwrap()
@@ -311,8 +326,11 @@ mod tests {
             .unwrap();
         {
             // Setup the bad database
-            let corrupt_db =
-                open_db::<CorruptedDB>(tempdir.path(), &release_channel::ReleaseChannel::Dev).await;
+            let corrupt_db = open_db::<CorruptedDB>(
+                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::<GoodDB>(
                     tmp_path.as_path(),
-                    &release_channel::ReleaseChannel::Dev,
+                    &release_channel::ReleaseChannel::Dev.dev_name(),
                 ));
                 assert!(
                     good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()

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<Option<String>> {
+            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 = (?)
+        }
+    }
+}

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<String>,
-    metrics_id: Option<Arc<str>>,
     installation_id: Option<Arc<str>>,
+    metrics_id: Option<Arc<str>>,
     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),
         };

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<String>,
     /// Identifier unique to each Zed installation (differs for stable, preview, dev)
     pub installation_id: Option<String>,
     /// Identifier unique to each logged in Zed user (randomly generated on first sign in)
-    pub metrics_id: Option<String>,
     /// Identifier unique to each Zed session (differs for each time you open Zed)
     pub session_id: Option<String>,
+    pub metrics_id: Option<String>,
     /// True for Zed staff, otherwise false
     pub is_staff: Option<bool>,
     /// 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<String>,
     /// Identifier unique to each Zed installation (differs for stable, preview, dev)
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub installation_id: Option<String>,
     /// Identifier unique to each Zed session (differs for each time you open Zed)
     pub session_id: String,

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<Client>, cx: &AsyncAppContext) -> Result<()> {
     Ok::<_, anyhow::Error>(())
 }
 
-async fn installation_id() -> Result<(String, bool)> {
+async fn system_id() -> Result<IdType> {
+    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<IdType> {
     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<String>,
 }
 
+#[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<String> {
     match std::fs::canonicalize(Path::new(&arg)) {
         Ok(path) => Ok(format!(

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<String>,
     app_version: SemanticVersion,
+    system_id: Option<String>,
+    installation_id: Option<String>,
     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(),
         };