Add system_id (#18040)

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
This commit is contained in:
Joseph T. Lyons 2024-09-19 07:20:27 -04:00 committed by GitHub
parent 5e6d1814e5
commit ca4980df02
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 184 additions and 62 deletions

View file

@ -37,9 +37,10 @@ pub struct Telemetry {
struct TelemetryState { struct TelemetryState {
settings: TelemetrySettings, 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) installation_id: Option<Arc<str>>, // Per app installation (different for dev, nightly, preview, and stable)
session_id: Option<String>, // Per app launch session_id: Option<String>, // Per app launch
metrics_id: Option<Arc<str>>, // Per logged-in user
release_channel: Option<&'static str>, release_channel: Option<&'static str>,
architecture: &'static str, architecture: &'static str,
events_queue: Vec<EventWrapper>, events_queue: Vec<EventWrapper>,
@ -191,9 +192,10 @@ impl Telemetry {
settings: *TelemetrySettings::get_global(cx), settings: *TelemetrySettings::get_global(cx),
architecture: env::consts::ARCH, architecture: env::consts::ARCH,
release_channel, release_channel,
system_id: None,
installation_id: None, installation_id: None,
metrics_id: None,
session_id: None, session_id: None,
metrics_id: None,
events_queue: Vec::new(), events_queue: Vec::new(),
flush_events_task: None, flush_events_task: None,
log_file: None, log_file: None,
@ -283,11 +285,13 @@ impl Telemetry {
pub fn start( pub fn start(
self: &Arc<Self>, self: &Arc<Self>,
system_id: Option<String>,
installation_id: Option<String>, installation_id: Option<String>,
session_id: String, session_id: String,
cx: &mut AppContext, cx: &mut AppContext,
) { ) {
let mut state = self.state.lock(); 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.installation_id = installation_id.map(|id| id.into());
state.session_id = Some(session_id); state.session_id = Some(session_id);
state.app_version = release_channel::AppVersion::global(cx).to_string(); state.app_version = release_channel::AppVersion::global(cx).to_string();
@ -637,9 +641,10 @@ impl Telemetry {
let state = this.state.lock(); let state = this.state.lock();
let request_body = EventRequestBody { let request_body = EventRequestBody {
system_id: state.system_id.as_deref().map(Into::into),
installation_id: state.installation_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(), session_id: state.session_id.clone(),
metrics_id: state.metrics_id.as_deref().map(Into::into),
is_staff: state.is_staff, is_staff: state.is_staff,
app_version: state.app_version.clone(), app_version: state.app_version.clone(),
os_name: state.os_name.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(), Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(),
)); ));
let http = FakeHttpClient::with_200_response(); let http = FakeHttpClient::with_200_response();
let system_id = Some("system_id".to_string());
let installation_id = Some("installation_id".to_string()); let installation_id = Some("installation_id".to_string());
let session_id = "session_id".to_string(); let session_id = "session_id".to_string();
@ -718,7 +724,7 @@ mod tests {
let telemetry = Telemetry::new(clock.clone(), http, cx); let telemetry = Telemetry::new(clock.clone(), http, cx);
telemetry.state.lock().max_queue_size = 4; 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)); assert!(is_empty_state(&telemetry));
@ -796,13 +802,14 @@ mod tests {
Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(), Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(),
)); ));
let http = FakeHttpClient::with_200_response(); let http = FakeHttpClient::with_200_response();
let system_id = Some("system_id".to_string());
let installation_id = Some("installation_id".to_string()); let installation_id = Some("installation_id".to_string());
let session_id = "session_id".to_string(); let session_id = "session_id".to_string();
cx.update(|cx| { cx.update(|cx| {
let telemetry = Telemetry::new(clock.clone(), http, cx); let telemetry = Telemetry::new(clock.clone(), http, cx);
telemetry.state.lock().max_queue_size = 4; 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)); assert!(is_empty_state(&telemetry));

View file

@ -149,7 +149,8 @@ pub async fn post_crash(
installation_id = %installation_id, installation_id = %installation_id,
description = %description, description = %description,
backtrace = %summary, backtrace = %summary,
"crash report"); "crash report"
);
if let Some(slack_panics_webhook) = app.config.slack_panics_webhook.clone() { if let Some(slack_panics_webhook) = app.config.slack_panics_webhook.clone() {
let payload = slack::WebhookBody::new(|w| { let payload = slack::WebhookBody::new(|w| {
@ -627,7 +628,9 @@ where
#[derive(Serialize, Debug, clickhouse::Row)] #[derive(Serialize, Debug, clickhouse::Row)]
pub struct EditorEventRow { pub struct EditorEventRow {
system_id: String,
installation_id: String, installation_id: String,
session_id: Option<String>,
metrics_id: String, metrics_id: String,
operation: String, operation: String,
app_version: String, app_version: String,
@ -647,7 +650,6 @@ pub struct EditorEventRow {
historical_event: bool, historical_event: bool,
architecture: String, architecture: String,
is_staff: Option<bool>, is_staff: Option<bool>,
session_id: Option<String>,
major: Option<i32>, major: Option<i32>,
minor: Option<i32>, minor: Option<i32>,
patch: Option<i32>, patch: Option<i32>,
@ -677,9 +679,10 @@ impl EditorEventRow {
os_name: body.os_name.clone(), os_name: body.os_name.clone(),
os_version: body.os_version.clone().unwrap_or_default(), os_version: body.os_version.clone().unwrap_or_default(),
architecture: body.architecture.clone(), architecture: body.architecture.clone(),
system_id: body.system_id.clone().unwrap_or_default(),
installation_id: body.installation_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(), session_id: body.session_id.clone(),
metrics_id: body.metrics_id.clone().unwrap_or_default(),
is_staff: body.is_staff, is_staff: body.is_staff,
time: time.timestamp_millis(), time: time.timestamp_millis(),
operation: event.operation, operation: event.operation,
@ -699,6 +702,7 @@ impl EditorEventRow {
#[derive(Serialize, Debug, clickhouse::Row)] #[derive(Serialize, Debug, clickhouse::Row)]
pub struct InlineCompletionEventRow { pub struct InlineCompletionEventRow {
installation_id: String, installation_id: String,
session_id: Option<String>,
provider: String, provider: String,
suggestion_accepted: bool, suggestion_accepted: bool,
app_version: String, app_version: String,
@ -713,7 +717,6 @@ pub struct InlineCompletionEventRow {
city: String, city: String,
time: i64, time: i64,
is_staff: Option<bool>, is_staff: Option<bool>,
session_id: Option<String>,
major: Option<i32>, major: Option<i32>,
minor: Option<i32>, minor: Option<i32>,
patch: Option<i32>, patch: Option<i32>,
@ -879,7 +882,9 @@ impl AssistantEventRow {
#[derive(Debug, clickhouse::Row, Serialize)] #[derive(Debug, clickhouse::Row, Serialize)]
pub struct CpuEventRow { pub struct CpuEventRow {
system_id: Option<String>,
installation_id: Option<String>, installation_id: Option<String>,
session_id: Option<String>,
is_staff: Option<bool>, is_staff: Option<bool>,
usage_as_percentage: f32, usage_as_percentage: f32,
core_count: u32, core_count: u32,
@ -888,7 +893,6 @@ pub struct CpuEventRow {
os_name: String, os_name: String,
os_version: String, os_version: String,
time: i64, time: i64,
session_id: Option<String>,
// pub normalized_cpu_usage: f64, MATERIALIZED // pub normalized_cpu_usage: f64, MATERIALIZED
major: Option<i32>, major: Option<i32>,
minor: Option<i32>, minor: Option<i32>,
@ -917,6 +921,7 @@ impl CpuEventRow {
release_channel: body.release_channel.clone().unwrap_or_default(), release_channel: body.release_channel.clone().unwrap_or_default(),
os_name: body.os_name.clone(), os_name: body.os_name.clone(),
os_version: body.os_version.clone().unwrap_or_default(), os_version: body.os_version.clone().unwrap_or_default(),
system_id: body.system_id.clone(),
installation_id: body.installation_id.clone(), installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(), session_id: body.session_id.clone(),
is_staff: body.is_staff, is_staff: body.is_staff,
@ -940,6 +945,7 @@ pub struct MemoryEventRow {
os_version: String, os_version: String,
// ClientEventBase // ClientEventBase
system_id: Option<String>,
installation_id: Option<String>, installation_id: Option<String>,
session_id: Option<String>, session_id: Option<String>,
is_staff: Option<bool>, is_staff: Option<bool>,
@ -971,6 +977,7 @@ impl MemoryEventRow {
release_channel: body.release_channel.clone().unwrap_or_default(), release_channel: body.release_channel.clone().unwrap_or_default(),
os_name: body.os_name.clone(), os_name: body.os_name.clone(),
os_version: body.os_version.clone().unwrap_or_default(), os_version: body.os_version.clone().unwrap_or_default(),
system_id: body.system_id.clone(),
installation_id: body.installation_id.clone(), installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(), session_id: body.session_id.clone(),
is_staff: body.is_staff, is_staff: body.is_staff,
@ -994,6 +1001,7 @@ pub struct AppEventRow {
os_version: String, os_version: String,
// ClientEventBase // ClientEventBase
system_id: Option<String>,
installation_id: Option<String>, installation_id: Option<String>,
session_id: Option<String>, session_id: Option<String>,
is_staff: Option<bool>, is_staff: Option<bool>,
@ -1024,6 +1032,7 @@ impl AppEventRow {
release_channel: body.release_channel.clone().unwrap_or_default(), release_channel: body.release_channel.clone().unwrap_or_default(),
os_name: body.os_name.clone(), os_name: body.os_name.clone(),
os_version: body.os_version.clone().unwrap_or_default(), os_version: body.os_version.clone().unwrap_or_default(),
system_id: body.system_id.clone(),
installation_id: body.installation_id.clone(), installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(), session_id: body.session_id.clone(),
is_staff: body.is_staff, is_staff: body.is_staff,
@ -1046,6 +1055,7 @@ pub struct SettingEventRow {
os_version: String, os_version: String,
// ClientEventBase // ClientEventBase
system_id: Option<String>,
installation_id: Option<String>, installation_id: Option<String>,
session_id: Option<String>, session_id: Option<String>,
is_staff: Option<bool>, is_staff: Option<bool>,
@ -1076,6 +1086,7 @@ impl SettingEventRow {
release_channel: body.release_channel.clone().unwrap_or_default(), release_channel: body.release_channel.clone().unwrap_or_default(),
os_name: body.os_name.clone(), os_name: body.os_name.clone(),
os_version: body.os_version.clone().unwrap_or_default(), os_version: body.os_version.clone().unwrap_or_default(),
system_id: body.system_id.clone(),
installation_id: body.installation_id.clone(), installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(), session_id: body.session_id.clone(),
is_staff: body.is_staff, is_staff: body.is_staff,
@ -1099,6 +1110,7 @@ pub struct ExtensionEventRow {
os_version: String, os_version: String,
// ClientEventBase // ClientEventBase
system_id: Option<String>,
installation_id: Option<String>, installation_id: Option<String>,
session_id: Option<String>, session_id: Option<String>,
is_staff: Option<bool>, is_staff: Option<bool>,
@ -1134,6 +1146,7 @@ impl ExtensionEventRow {
release_channel: body.release_channel.clone().unwrap_or_default(), release_channel: body.release_channel.clone().unwrap_or_default(),
os_name: body.os_name.clone(), os_name: body.os_name.clone(),
os_version: body.os_version.clone().unwrap_or_default(), os_version: body.os_version.clone().unwrap_or_default(),
system_id: body.system_id.clone(),
installation_id: body.installation_id.clone(), installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(), session_id: body.session_id.clone(),
is_staff: body.is_staff, is_staff: body.is_staff,
@ -1224,6 +1237,7 @@ pub struct EditEventRow {
os_version: String, os_version: String,
// ClientEventBase // ClientEventBase
system_id: Option<String>,
installation_id: Option<String>, installation_id: Option<String>,
// Note: This column name has a typo in the ClickHouse table. // Note: This column name has a typo in the ClickHouse table.
#[serde(rename = "sesssion_id")] #[serde(rename = "sesssion_id")]
@ -1261,6 +1275,7 @@ impl EditEventRow {
release_channel: body.release_channel.clone().unwrap_or_default(), release_channel: body.release_channel.clone().unwrap_or_default(),
os_name: body.os_name.clone(), os_name: body.os_name.clone(),
os_version: body.os_version.clone().unwrap_or_default(), os_version: body.os_version.clone().unwrap_or_default(),
system_id: body.system_id.clone(),
installation_id: body.installation_id.clone(), installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(), session_id: body.session_id.clone(),
is_staff: body.is_staff, is_staff: body.is_staff,

View file

@ -11,16 +11,14 @@ pub use smol;
pub use sqlez; pub use sqlez;
pub use sqlez_macros; pub use sqlez_macros;
use release_channel::ReleaseChannel;
pub use release_channel::RELEASE_CHANNEL; pub use release_channel::RELEASE_CHANNEL;
use sqlez::domain::Migrator; use sqlez::domain::Migrator;
use sqlez::thread_safe_connection::ThreadSafeConnection; use sqlez::thread_safe_connection::ThreadSafeConnection;
use sqlez_macros::sql; use sqlez_macros::sql;
use std::env;
use std::future::Future; use std::future::Future;
use std::path::Path; use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{atomic::Ordering, LazyLock};
use std::sync::LazyLock; use std::{env, sync::atomic::AtomicBool};
use util::{maybe, ResultExt}; use util::{maybe, ResultExt};
const CONNECTION_INITIALIZE_QUERY: &str = sql!( 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 /// 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. /// 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. /// In either case, static variables are set so that the user can be notified.
pub async fn open_db<M: Migrator + 'static>( pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, scope: &str) -> ThreadSafeConnection<M> {
db_dir: &Path,
release_channel: &ReleaseChannel,
) -> ThreadSafeConnection<M> {
if *ZED_STATELESS { if *ZED_STATELESS {
return open_fallback_db().await; return open_fallback_db().await;
} }
let release_channel_name = release_channel.dev_name(); let main_db_dir = db_dir.join(format!("0-{}", scope));
let main_db_dir = db_dir.join(Path::new(&format!("0-{}", release_channel_name)));
let connection = maybe!(async { let connection = maybe!(async {
smol::fs::create_dir_all(&main_db_dir) 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 /// Implements a basic DB wrapper for a given domain
#[macro_export] #[macro_export]
macro_rules! define_connection { 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>); pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection<$t>);
impl ::std::ops::Deref for $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"))] #[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)))) $t($crate::smol::block_on($crate::open_test_db(stringify!($id))))
}); });
#[cfg(not(any(test, feature = "test-support")))] #[cfg(not(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_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)))
}); });
}; };
(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 )>); pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection<( $($d),+, $t )>);
impl ::std::ops::Deref for $t { impl ::std::ops::Deref for $t {
@ -178,7 +177,13 @@ macro_rules! define_connection {
#[cfg(not(any(test, feature = "test-support")))] #[cfg(not(any(test, feature = "test-support")))]
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { 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") .prefix("DbTests")
.tempdir() .tempdir()
.unwrap(); .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) /// Test that DB exists but corrupted (causing recreate)
@ -262,13 +271,19 @@ mod tests {
.tempdir() .tempdir()
.unwrap(); .unwrap();
{ {
let corrupt_db = let corrupt_db = open_db::<CorruptedDB>(
open_db::<CorruptedDB>(tempdir.path(), &release_channel::ReleaseChannel::Dev).await; tempdir.path(),
&release_channel::ReleaseChannel::Dev.dev_name(),
)
.await;
assert!(corrupt_db.persistent()); assert!(corrupt_db.persistent());
} }
let good_db = let good_db = open_db::<GoodDB>(
open_db::<GoodDB>(tempdir.path(), &release_channel::ReleaseChannel::Dev).await; tempdir.path(),
&release_channel::ReleaseChannel::Dev.dev_name(),
)
.await;
assert!( assert!(
good_db.select_row::<usize>("SELECT * FROM test2").unwrap()() good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
.unwrap() .unwrap()
@ -311,8 +326,11 @@ mod tests {
.unwrap(); .unwrap();
{ {
// Setup the bad database // Setup the bad database
let corrupt_db = let corrupt_db = open_db::<CorruptedDB>(
open_db::<CorruptedDB>(tempdir.path(), &release_channel::ReleaseChannel::Dev).await; tempdir.path(),
&release_channel::ReleaseChannel::Dev.dev_name(),
)
.await;
assert!(corrupt_db.persistent()); assert!(corrupt_db.persistent());
} }
@ -323,7 +341,7 @@ mod tests {
let guard = thread::spawn(move || { let guard = thread::spawn(move || {
let good_db = smol::block_on(open_db::<GoodDB>( let good_db = smol::block_on(open_db::<GoodDB>(
tmp_path.as_path(), tmp_path.as_path(),
&release_channel::ReleaseChannel::Dev, &release_channel::ReleaseChannel::Dev.dev_name(),
)); ));
assert!( assert!(
good_db.select_row::<usize>("SELECT * FROM test2").unwrap()() good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()

View file

@ -60,3 +60,33 @@ mod tests {
assert_eq!(db.read_kvp("key-1").unwrap(), None); 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 = (?)
}
}
}

View file

@ -44,8 +44,8 @@ const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
struct FeedbackRequestBody<'a> { struct FeedbackRequestBody<'a> {
feedback_text: &'a str, feedback_text: &'a str,
email: Option<String>, email: Option<String>,
metrics_id: Option<Arc<str>>,
installation_id: Option<Arc<str>>, installation_id: Option<Arc<str>>,
metrics_id: Option<Arc<str>>,
system_specs: SystemSpecs, system_specs: SystemSpecs,
is_staff: bool, is_staff: bool,
} }
@ -296,16 +296,16 @@ impl FeedbackModal {
} }
let telemetry = zed_client.telemetry(); let telemetry = zed_client.telemetry();
let metrics_id = telemetry.metrics_id();
let installation_id = telemetry.installation_id(); let installation_id = telemetry.installation_id();
let metrics_id = telemetry.metrics_id();
let is_staff = telemetry.is_staff(); let is_staff = telemetry.is_staff();
let http_client = zed_client.http_client(); let http_client = zed_client.http_client();
let feedback_endpoint = http_client.build_url("/api/feedback"); let feedback_endpoint = http_client.build_url("/api/feedback");
let request = FeedbackRequestBody { let request = FeedbackRequestBody {
feedback_text, feedback_text,
email, email,
metrics_id,
installation_id, installation_id,
metrics_id,
system_specs, system_specs,
is_staff: is_staff.unwrap_or(false), is_staff: is_staff.unwrap_or(false),
}; };

View file

@ -5,12 +5,14 @@ use std::{fmt::Display, sync::Arc, time::Duration};
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct EventRequestBody { 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) /// Identifier unique to each Zed installation (differs for stable, preview, dev)
pub installation_id: Option<String>, pub installation_id: Option<String>,
/// Identifier unique to each logged in Zed user (randomly generated on first sign in) /// 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) /// Identifier unique to each Zed session (differs for each time you open Zed)
pub session_id: Option<String>, pub session_id: Option<String>,
pub metrics_id: Option<String>,
/// True for Zed staff, otherwise false /// True for Zed staff, otherwise false
pub is_staff: Option<bool>, pub is_staff: Option<bool>,
/// Zed version number /// Zed version number
@ -34,6 +36,7 @@ pub struct EventWrapper {
pub signed_in: bool, pub signed_in: bool,
/// Duration between this event's timestamp and the timestamp of the first event in the current batch /// Duration between this event's timestamp and the timestamp of the first event in the current batch
pub milliseconds_since_first_event: i64, pub milliseconds_since_first_event: i64,
/// The event itself
#[serde(flatten)] #[serde(flatten)]
pub event: Event, pub event: Event,
} }
@ -245,8 +248,11 @@ pub struct Panic {
pub architecture: String, pub architecture: String,
/// The time the panic occurred (UNIX millisecond timestamp) /// The time the panic occurred (UNIX millisecond timestamp)
pub panicked_on: i64, pub panicked_on: i64,
/// Identifier unique to each system Zed is installed on
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub system_id: Option<String>,
/// Identifier unique to each Zed installation (differs for stable, preview, dev) /// Identifier unique to each Zed installation (differs for stable, preview, dev)
#[serde(skip_serializing_if = "Option::is_none")]
pub installation_id: Option<String>, pub installation_id: Option<String>,
/// Identifier unique to each Zed session (differs for each time you open Zed) /// Identifier unique to each Zed session (differs for each time you open Zed)
pub session_id: String, pub session_id: String,

View file

@ -13,7 +13,7 @@ use clap::{command, Parser};
use cli::FORCE_CLI_MODE_ENV_VAR_NAME; use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use client::{parse_zed_link, Client, DevServerToken, ProxySettings, UserStore}; use client::{parse_zed_link, Client, DevServerToken, ProxySettings, UserStore};
use collab_ui::channel_view::ChannelView; 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 editor::Editor;
use env_logger::Builder; use env_logger::Builder;
use fs::{Fs, RealFs}; use fs::{Fs, RealFs};
@ -334,19 +334,17 @@ fn main() {
.with_assets(Assets) .with_assets(Assets)
.with_http_client(IsahcHttpClient::new(None, None)); .with_http_client(IsahcHttpClient::new(None, None));
let (installation_id, existing_installation_id_found) = app let system_id = app.background_executor().block(system_id()).ok();
.background_executor() let installation_id = app.background_executor().block(installation_id()).ok();
.block(installation_id()) let session_id = Uuid::new_v4().to_string();
.ok()
.unzip();
let session = app.background_executor().block(Session::new()); let session = app.background_executor().block(Session::new());
let app_version = AppVersion::init(env!("CARGO_PKG_VERSION")); let app_version = AppVersion::init(env!("CARGO_PKG_VERSION"));
reliability::init_panic_hook( reliability::init_panic_hook(
installation_id.clone(),
app_version, 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(); let (open_listener, mut open_rx) = OpenListener::new();
@ -491,14 +489,26 @@ fn main() {
client::init(&client, cx); client::init(&client, cx);
language::init(cx); language::init(cx);
let telemetry = client.telemetry(); let telemetry = client.telemetry();
telemetry.start(installation_id.clone(), session.id().to_owned(), cx); telemetry.start(
telemetry.report_app_event( system_id.as_ref().map(|id| id.to_string()),
match existing_installation_id_found { installation_id.as_ref().map(|id| id.to_string()),
Some(false) => "first open", session_id,
_ => "open", cx,
}
.to_string(),
); );
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_session = cx.new_model(|cx| AppSession::new(session, cx));
let app_state = Arc::new(AppState { let app_state = Arc::new(AppState {
@ -514,7 +524,11 @@ fn main() {
AppState::set_global(Arc::downgrade(&app_state), cx); AppState::set_global(Arc::downgrade(&app_state), cx);
auto_update::init(client.http_client(), 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 prompt_builder = init_common(app_state.clone(), cx);
let args = Args::parse(); let args = Args::parse();
@ -755,7 +769,23 @@ async fn authenticate(client: Arc<Client>, cx: &AsyncAppContext) -> Result<()> {
Ok::<_, anyhow::Error>(()) 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 legacy_key_name = "device_id".to_string();
let key_name = "installation_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()) .write_kvp(key_name, installation_id.clone())
.await?; .await?;
KEY_VALUE_STORE.delete_kvp(legacy_key_name).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) { 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(); 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()) .write_kvp(key_name, installation_id.clone())
.await?; .await?;
Ok((installation_id, false)) Ok(IdType::New(installation_id))
} }
async fn restore_or_create_workspace( async fn restore_or_create_workspace(
@ -1087,6 +1117,20 @@ struct Args {
dev_server_token: Option<String>, 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> { fn parse_url_arg(arg: &str, cx: &AppContext) -> Result<String> {
match std::fs::canonicalize(Path::new(&arg)) { match std::fs::canonicalize(Path::new(&arg)) {
Ok(path) => Ok(format!( Ok(path) => Ok(format!(

View file

@ -28,8 +28,9 @@ use crate::stdout_is_a_pty;
static PANIC_COUNT: AtomicU32 = AtomicU32::new(0); static PANIC_COUNT: AtomicU32 = AtomicU32::new(0);
pub fn init_panic_hook( pub fn init_panic_hook(
installation_id: Option<String>,
app_version: SemanticVersion, app_version: SemanticVersion,
system_id: Option<String>,
installation_id: Option<String>,
session_id: String, session_id: String,
) { ) {
let is_pty = stdout_is_a_pty(); let is_pty = stdout_is_a_pty();
@ -102,6 +103,7 @@ pub fn init_panic_hook(
architecture: env::consts::ARCH.into(), architecture: env::consts::ARCH.into(),
panicked_on: Utc::now().timestamp_millis(), panicked_on: Utc::now().timestamp_millis(),
backtrace, backtrace,
system_id: system_id.clone(),
installation_id: installation_id.clone(), installation_id: installation_id.clone(),
session_id: session_id.clone(), session_id: session_id.clone(),
}; };