Merge pull request #1670 from zed-industries/metrics-id-uuid

Max Brunsfeld created

Identify users in amplitude via a separate 'metrics_id' UUID

Change summary

crates/client/src/client.rs                                     |   4 
crates/client/src/telemetry.rs                                  |  10 
crates/client/src/test.rs                                       |  64 
crates/client/src/user.rs                                       |  10 
crates/collab/migrations/20220913211150_create_signups.down.sql |   6 
crates/collab/migrations/20220913211150_create_signups.sql      |   0 
crates/collab/migrations/20220929182110_add_metrics_id.sql      |   2 
crates/collab/src/api.rs                                        |  82 
crates/collab/src/db.rs                                         |  56 
crates/collab/src/db_tests.rs                                   | 349 +-
crates/collab/src/integration_tests.rs                          |   7 
crates/collab/src/rpc.rs                                        |  17 
crates/contacts_panel/src/contacts_panel.rs                     |  11 
crates/rpc/proto/zed.proto                                      |   9 
crates/rpc/src/proto.rs                                         |   3 
15 files changed, 370 insertions(+), 260 deletions(-)

Detailed changes

crates/client/src/client.rs 🔗

@@ -320,11 +320,9 @@ impl Client {
         log::info!("set status on client {}: {:?}", self.id, status);
         let mut state = self.state.write();
         *state.status.0.borrow_mut() = status;
-        let user_id = state.credentials.as_ref().map(|c| c.user_id);
 
         match status {
             Status::Connected { .. } => {
-                self.telemetry.set_user_id(user_id);
                 state._reconnect_task = None;
             }
             Status::ConnectionLost => {
@@ -353,7 +351,7 @@ impl Client {
                 }));
             }
             Status::SignedOut | Status::UpgradeRequired => {
-                self.telemetry.set_user_id(user_id);
+                self.telemetry.set_metrics_id(None);
                 state._reconnect_task.take();
             }
             _ => {}

crates/client/src/telemetry.rs 🔗

@@ -29,7 +29,7 @@ pub struct Telemetry {
 
 #[derive(Default)]
 struct TelemetryState {
-    user_id: Option<Arc<str>>,
+    metrics_id: Option<Arc<str>>,
     device_id: Option<Arc<str>>,
     app_version: Option<Arc<str>>,
     os_version: Option<Arc<str>>,
@@ -115,7 +115,7 @@ impl Telemetry {
                 flush_task: Default::default(),
                 next_event_id: 0,
                 log_file: None,
-                user_id: None,
+                metrics_id: None,
             }),
         });
 
@@ -176,8 +176,8 @@ impl Telemetry {
             .detach();
     }
 
-    pub fn set_user_id(&self, user_id: Option<u64>) {
-        self.state.lock().user_id = user_id.map(|id| id.to_string().into());
+    pub fn set_metrics_id(&self, metrics_id: Option<String>) {
+        self.state.lock().metrics_id = metrics_id.map(|s| s.into());
     }
 
     pub fn report_event(self: &Arc<Self>, kind: &str, properties: Value) {
@@ -199,7 +199,7 @@ impl Telemetry {
                 None
             },
             user_properties: None,
-            user_id: state.user_id.clone(),
+            user_id: state.metrics_id.clone(),
             device_id: state.device_id.clone(),
             os_name: state.os_name,
             os_version: state.os_version.clone(),

crates/client/src/test.rs 🔗

@@ -6,7 +6,10 @@ use anyhow::{anyhow, Result};
 use futures::{future::BoxFuture, stream::BoxStream, Future, StreamExt};
 use gpui::{executor, ModelHandle, TestAppContext};
 use parking_lot::Mutex;
-use rpc::{proto, ConnectionId, Peer, Receipt, TypedEnvelope};
+use rpc::{
+    proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse},
+    ConnectionId, Peer, Receipt, TypedEnvelope,
+};
 use std::{fmt, rc::Rc, sync::Arc};
 
 pub struct FakeServer {
@@ -93,6 +96,7 @@ impl FakeServer {
             .authenticate_and_connect(false, &cx.to_async())
             .await
             .unwrap();
+
         server
     }
 
@@ -126,26 +130,44 @@ impl FakeServer {
     #[allow(clippy::await_holding_lock)]
     pub async fn receive<M: proto::EnvelopedMessage>(&self) -> Result<TypedEnvelope<M>> {
         self.executor.start_waiting();
-        let message = self
-            .state
-            .lock()
-            .incoming
-            .as_mut()
-            .expect("not connected")
-            .next()
-            .await
-            .ok_or_else(|| anyhow!("other half hung up"))?;
-        self.executor.finish_waiting();
-        let type_name = message.payload_type_name();
-        Ok(*message
-            .into_any()
-            .downcast::<TypedEnvelope<M>>()
-            .unwrap_or_else(|_| {
-                panic!(
-                    "fake server received unexpected message type: {:?}",
-                    type_name
-                );
-            }))
+
+        loop {
+            let message = self
+                .state
+                .lock()
+                .incoming
+                .as_mut()
+                .expect("not connected")
+                .next()
+                .await
+                .ok_or_else(|| anyhow!("other half hung up"))?;
+            self.executor.finish_waiting();
+            let type_name = message.payload_type_name();
+            let message = message.into_any();
+
+            if message.is::<TypedEnvelope<M>>() {
+                return Ok(*message.downcast().unwrap());
+            }
+
+            if message.is::<TypedEnvelope<GetPrivateUserInfo>>() {
+                self.respond(
+                    message
+                        .downcast::<TypedEnvelope<GetPrivateUserInfo>>()
+                        .unwrap()
+                        .receipt(),
+                    GetPrivateUserInfoResponse {
+                        metrics_id: "the-metrics-id".into(),
+                    },
+                )
+                .await;
+                continue;
+            }
+
+            panic!(
+                "fake server received unexpected message type: {:?}",
+                type_name
+            );
+        }
     }
 
     pub async fn respond<T: proto::RequestMessage>(

crates/client/src/user.rs 🔗

@@ -142,10 +142,14 @@ impl UserStore {
                     match status {
                         Status::Connected { .. } => {
                             if let Some((this, user_id)) = this.upgrade(&cx).zip(client.user_id()) {
-                                let user = this
+                                let fetch_user = this
                                     .update(&mut cx, |this, cx| this.fetch_user(user_id, cx))
-                                    .log_err()
-                                    .await;
+                                    .log_err();
+                                let fetch_metrics_id =
+                                    client.request(proto::GetPrivateUserInfo {}).log_err();
+                                let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
+                                client.telemetry.set_metrics_id(info.map(|i| i.metrics_id));
+                                client.telemetry.report_event("sign in", Default::default());
                                 current_user_tx.send(user).await.ok();
                             }
                         }

crates/collab/src/api.rs 🔗

@@ -24,6 +24,7 @@ use tracing::instrument;
 
 pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> {
     Router::new()
+        .route("/user", get(get_authenticated_user))
         .route("/users", get(get_users).post(create_user))
         .route("/users/:id", put(update_user).delete(destroy_user))
         .route("/users/:id/access_tokens", post(create_access_token))
@@ -85,10 +86,33 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
     Ok::<_, Error>(next.run(req).await)
 }
 
+#[derive(Debug, Deserialize)]
+struct AuthenticatedUserParams {
+    github_user_id: i32,
+    github_login: String,
+}
+
+#[derive(Debug, Serialize)]
+struct AuthenticatedUserResponse {
+    user: User,
+    metrics_id: String,
+}
+
+async fn get_authenticated_user(
+    Query(params): Query<AuthenticatedUserParams>,
+    Extension(app): Extension<Arc<AppState>>,
+) -> Result<Json<AuthenticatedUserResponse>> {
+    let user = app
+        .db
+        .get_user_by_github_account(&params.github_login, Some(params.github_user_id))
+        .await?
+        .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "user not found".into()))?;
+    let metrics_id = app.db.get_user_metrics_id(user.id).await?;
+    return Ok(Json(AuthenticatedUserResponse { user, metrics_id }));
+}
+
 #[derive(Debug, Deserialize)]
 struct GetUsersQueryParams {
-    github_user_id: Option<i32>,
-    github_login: Option<String>,
     query: Option<String>,
     page: Option<u32>,
     limit: Option<u32>,
@@ -98,14 +122,6 @@ async fn get_users(
     Query(params): Query<GetUsersQueryParams>,
     Extension(app): Extension<Arc<AppState>>,
 ) -> Result<Json<Vec<User>>> {
-    if let Some(github_login) = &params.github_login {
-        let user = app
-            .db
-            .get_user_by_github_account(github_login, params.github_user_id)
-            .await?;
-        return Ok(Json(Vec::from_iter(user)));
-    }
-
     let limit = params.limit.unwrap_or(100);
     let users = if let Some(query) = params.query {
         app.db.fuzzy_search_users(&query, limit).await?
@@ -124,6 +140,8 @@ struct CreateUserParams {
     email_address: String,
     email_confirmation_code: Option<String>,
     #[serde(default)]
+    admin: bool,
+    #[serde(default)]
     invite_count: i32,
 }
 
@@ -131,6 +149,7 @@ struct CreateUserParams {
 struct CreateUserResponse {
     user: User,
     signup_device_id: Option<String>,
+    metrics_id: String,
 }
 
 async fn create_user(
@@ -143,12 +162,10 @@ async fn create_user(
         github_user_id: params.github_user_id,
         invite_count: params.invite_count,
     };
-    let user_id;
-    let signup_device_id;
+
     // Creating a user via the normal signup process
-    if let Some(email_confirmation_code) = params.email_confirmation_code {
-        let result = app
-            .db
+    let result = if let Some(email_confirmation_code) = params.email_confirmation_code {
+        app.db
             .create_user_from_invite(
                 &Invite {
                     email_address: params.email_address,
@@ -156,34 +173,37 @@ async fn create_user(
                 },
                 user,
             )
-            .await?;
-        user_id = result.user_id;
-        signup_device_id = result.signup_device_id;
-        if let Some(inviter_id) = result.inviting_user_id {
-            rpc_server
-                .invite_code_redeemed(inviter_id, user_id)
-                .await
-                .trace_err();
-        }
+            .await?
     }
     // Creating a user as an admin
-    else {
-        user_id = app
-            .db
+    else if params.admin {
+        app.db
             .create_user(&params.email_address, false, user)
-            .await?;
-        signup_device_id = None;
+            .await?
+    } else {
+        Err(Error::Http(
+            StatusCode::UNPROCESSABLE_ENTITY,
+            "email confirmation code is required".into(),
+        ))?
+    };
+
+    if let Some(inviter_id) = result.inviting_user_id {
+        rpc_server
+            .invite_code_redeemed(inviter_id, result.user_id)
+            .await
+            .trace_err();
     }
 
     let user = app
         .db
-        .get_user_by_id(user_id)
+        .get_user_by_id(result.user_id)
         .await?
         .ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
 
     Ok(Json(CreateUserResponse {
         user,
-        signup_device_id,
+        metrics_id: result.metrics_id,
+        signup_device_id: result.signup_device_id,
     }))
 }
 

crates/collab/src/db.rs 🔗

@@ -17,10 +17,11 @@ pub trait Db: Send + Sync {
         email_address: &str,
         admin: bool,
         params: NewUserParams,
-    ) -> Result<UserId>;
+    ) -> Result<NewUserResult>;
     async fn get_all_users(&self, page: u32, limit: u32) -> Result<Vec<User>>;
     async fn fuzzy_search_users(&self, query: &str, limit: u32) -> Result<Vec<User>>;
     async fn get_user_by_id(&self, id: UserId) -> Result<Option<User>>;
+    async fn get_user_metrics_id(&self, id: UserId) -> Result<String>;
     async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<User>>;
     async fn get_users_with_no_invites(&self, invited_by_another_user: bool) -> Result<Vec<User>>;
     async fn get_user_by_github_account(
@@ -208,21 +209,26 @@ impl Db for PostgresDb {
         email_address: &str,
         admin: bool,
         params: NewUserParams,
-    ) -> Result<UserId> {
+    ) -> Result<NewUserResult> {
         let query = "
             INSERT INTO users (email_address, github_login, github_user_id, admin)
             VALUES ($1, $2, $3, $4)
             ON CONFLICT (github_login) DO UPDATE SET github_login = excluded.github_login
-            RETURNING id
+            RETURNING id, metrics_id::text
         ";
-        Ok(sqlx::query_scalar(query)
+        let (user_id, metrics_id): (UserId, String) = sqlx::query_as(query)
             .bind(email_address)
             .bind(params.github_login)
             .bind(params.github_user_id)
             .bind(admin)
             .fetch_one(&self.pool)
-            .await
-            .map(UserId)?)
+            .await?;
+        Ok(NewUserResult {
+            user_id,
+            metrics_id,
+            signup_device_id: None,
+            inviting_user_id: None,
+        })
     }
 
     async fn get_all_users(&self, page: u32, limit: u32) -> Result<Vec<User>> {
@@ -256,6 +262,18 @@ impl Db for PostgresDb {
         Ok(users.into_iter().next())
     }
 
+    async fn get_user_metrics_id(&self, id: UserId) -> Result<String> {
+        let query = "
+            SELECT metrics_id::text
+            FROM users
+            WHERE id = $1
+        ";
+        Ok(sqlx::query_scalar(query)
+            .bind(id)
+            .fetch_one(&self.pool)
+            .await?)
+    }
+
     async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<User>> {
         let ids = ids.into_iter().map(|id| id.0).collect::<Vec<_>>();
         let query = "
@@ -493,13 +511,13 @@ impl Db for PostgresDb {
             ))?;
         }
 
-        let user_id: UserId = sqlx::query_scalar(
+        let (user_id, metrics_id): (UserId, String) = sqlx::query_as(
             "
             INSERT INTO users
             (email_address, github_login, github_user_id, admin, invite_count, invite_code)
             VALUES
             ($1, $2, $3, 'f', $4, $5)
-            RETURNING id
+            RETURNING id, metrics_id::text
             ",
         )
         .bind(&invite.email_address)
@@ -559,6 +577,7 @@ impl Db for PostgresDb {
         tx.commit().await?;
         Ok(NewUserResult {
             user_id,
+            metrics_id,
             inviting_user_id,
             signup_device_id,
         })
@@ -1722,6 +1741,7 @@ pub struct NewUserParams {
 #[derive(Debug)]
 pub struct NewUserResult {
     pub user_id: UserId,
+    pub metrics_id: String,
     pub inviting_user_id: Option<UserId>,
     pub signup_device_id: Option<String>,
 }
@@ -1808,15 +1828,15 @@ mod test {
             email_address: &str,
             admin: bool,
             params: NewUserParams,
-        ) -> Result<UserId> {
+        ) -> Result<NewUserResult> {
             self.background.simulate_random_delay().await;
 
             let mut users = self.users.lock();
-            if let Some(user) = users
+            let user_id = if let Some(user) = users
                 .values()
                 .find(|user| user.github_login == params.github_login)
             {
-                Ok(user.id)
+                user.id
             } else {
                 let id = post_inc(&mut *self.next_user_id.lock());
                 let user_id = UserId(id);
@@ -1833,8 +1853,14 @@ mod test {
                         connected_once: false,
                     },
                 );
-                Ok(user_id)
-            }
+                user_id
+            };
+            Ok(NewUserResult {
+                user_id,
+                metrics_id: "the-metrics-id".to_string(),
+                inviting_user_id: None,
+                signup_device_id: None,
+            })
         }
 
         async fn get_all_users(&self, _page: u32, _limit: u32) -> Result<Vec<User>> {
@@ -1850,6 +1876,10 @@ mod test {
             Ok(self.get_users_by_ids(vec![id]).await?.into_iter().next())
         }
 
+        async fn get_user_metrics_id(&self, _id: UserId) -> Result<String> {
+            Ok("the-metrics-id".to_string())
+        }
+
         async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<User>> {
             self.background.simulate_random_delay().await;
             let users = self.users.lock();

crates/collab/src/db_tests.rs 🔗

@@ -12,89 +12,56 @@ async fn test_get_users_by_ids() {
     ] {
         let db = test_db.db();
 
-        let user1 = db
-            .create_user(
-                "u1@example.com",
-                false,
-                NewUserParams {
-                    github_login: "u1".into(),
-                    github_user_id: 1,
-                    invite_count: 0,
-                },
-            )
-            .await
-            .unwrap();
-        let user2 = db
-            .create_user(
-                "u2@example.com",
-                false,
-                NewUserParams {
-                    github_login: "u2".into(),
-                    github_user_id: 2,
-                    invite_count: 0,
-                },
-            )
-            .await
-            .unwrap();
-        let user3 = db
-            .create_user(
-                "u3@example.com",
-                false,
-                NewUserParams {
-                    github_login: "u3".into(),
-                    github_user_id: 3,
-                    invite_count: 0,
-                },
-            )
-            .await
-            .unwrap();
-        let user4 = db
-            .create_user(
-                "u4@example.com",
-                false,
-                NewUserParams {
-                    github_login: "u4".into(),
-                    github_user_id: 4,
-                    invite_count: 0,
-                },
-            )
-            .await
-            .unwrap();
+        let mut user_ids = Vec::new();
+        for i in 1..=4 {
+            user_ids.push(
+                db.create_user(
+                    &format!("user{i}@example.com"),
+                    false,
+                    NewUserParams {
+                        github_login: format!("user{i}"),
+                        github_user_id: i,
+                        invite_count: 0,
+                    },
+                )
+                .await
+                .unwrap()
+                .user_id,
+            );
+        }
 
         assert_eq!(
-            db.get_users_by_ids(vec![user1, user2, user3, user4])
-                .await
-                .unwrap(),
+            db.get_users_by_ids(user_ids.clone()).await.unwrap(),
             vec![
                 User {
-                    id: user1,
-                    github_login: "u1".to_string(),
+                    id: user_ids[0],
+                    github_login: "user1".to_string(),
                     github_user_id: Some(1),
-                    email_address: Some("u1@example.com".to_string()),
+                    email_address: Some("user1@example.com".to_string()),
                     admin: false,
                     ..Default::default()
                 },
                 User {
-                    id: user2,
-                    github_login: "u2".to_string(),
+                    id: user_ids[1],
+                    github_login: "user2".to_string(),
                     github_user_id: Some(2),
-                    email_address: Some("u2@example.com".to_string()),
+                    email_address: Some("user2@example.com".to_string()),
                     admin: false,
                     ..Default::default()
                 },
                 User {
-                    id: user3,
-                    github_login: "u3".to_string(),
+                    id: user_ids[2],
+                    github_login: "user3".to_string(),
                     github_user_id: Some(3),
-                    email_address: Some("u3@example.com".to_string()),
+                    email_address: Some("user3@example.com".to_string()),
                     admin: false,
                     ..Default::default()
                 },
                 User {
-                    id: user4,
-                    github_login: "u4".to_string(),
+                    id: user_ids[3],
+                    github_login: "user4".to_string(),
                     github_user_id: Some(4),
-                    email_address: Some("u4@example.com".to_string()),
+                    email_address: Some("user4@example.com".to_string()),
                     admin: false,
                     ..Default::default()
                 }
@@ -121,7 +88,8 @@ async fn test_get_user_by_github_account() {
                 },
             )
             .await
-            .unwrap();
+            .unwrap()
+            .user_id;
         let user_id2 = db
             .create_user(
                 "user2@example.com",
@@ -133,7 +101,8 @@ async fn test_get_user_by_github_account() {
                 },
             )
             .await
-            .unwrap();
+            .unwrap()
+            .user_id;
 
         let user = db
             .get_user_by_github_account("login1", None)
@@ -177,7 +146,8 @@ async fn test_worktree_extensions() {
             },
         )
         .await
-        .unwrap();
+        .unwrap()
+        .user_id;
     let project = db.register_project(user).await.unwrap();
 
     db.update_worktree_extensions(project, 100, Default::default())
@@ -237,43 +207,25 @@ async fn test_user_activity() {
     let test_db = TestDb::postgres().await;
     let db = test_db.db();
 
-    let user_1 = db
-        .create_user(
-            "u1@example.com",
-            false,
-            NewUserParams {
-                github_login: "u1".into(),
-                github_user_id: 0,
-                invite_count: 0,
-            },
-        )
-        .await
-        .unwrap();
-    let user_2 = db
-        .create_user(
-            "u2@example.com",
-            false,
-            NewUserParams {
-                github_login: "u2".into(),
-                github_user_id: 0,
-                invite_count: 0,
-            },
-        )
-        .await
-        .unwrap();
-    let user_3 = db
-        .create_user(
-            "u3@example.com",
-            false,
-            NewUserParams {
-                github_login: "u3".into(),
-                github_user_id: 0,
-                invite_count: 0,
-            },
-        )
-        .await
-        .unwrap();
-    let project_1 = db.register_project(user_1).await.unwrap();
+    let mut user_ids = Vec::new();
+    for i in 0..=2 {
+        user_ids.push(
+            db.create_user(
+                &format!("user{i}@example.com"),
+                false,
+                NewUserParams {
+                    github_login: format!("user{i}"),
+                    github_user_id: i,
+                    invite_count: 0,
+                },
+            )
+            .await
+            .unwrap()
+            .user_id,
+        );
+    }
+
+    let project_1 = db.register_project(user_ids[0]).await.unwrap();
     db.update_worktree_extensions(
         project_1,
         1,
@@ -281,34 +233,37 @@ async fn test_user_activity() {
     )
     .await
     .unwrap();
-    let project_2 = db.register_project(user_2).await.unwrap();
+    let project_2 = db.register_project(user_ids[1]).await.unwrap();
     let t0 = OffsetDateTime::now_utc() - Duration::from_secs(60 * 60);
 
     // User 2 opens a project
     let t1 = t0 + Duration::from_secs(10);
-    db.record_user_activity(t0..t1, &[(user_2, project_2)])
+    db.record_user_activity(t0..t1, &[(user_ids[1], project_2)])
         .await
         .unwrap();
 
     let t2 = t1 + Duration::from_secs(10);
-    db.record_user_activity(t1..t2, &[(user_2, project_2)])
+    db.record_user_activity(t1..t2, &[(user_ids[1], project_2)])
         .await
         .unwrap();
 
     // User 1 joins the project
     let t3 = t2 + Duration::from_secs(10);
-    db.record_user_activity(t2..t3, &[(user_2, project_2), (user_1, project_2)])
-        .await
-        .unwrap();
+    db.record_user_activity(
+        t2..t3,
+        &[(user_ids[1], project_2), (user_ids[0], project_2)],
+    )
+    .await
+    .unwrap();
 
     // User 1 opens another project
     let t4 = t3 + Duration::from_secs(10);
     db.record_user_activity(
         t3..t4,
         &[
-            (user_2, project_2),
-            (user_1, project_2),
-            (user_1, project_1),
+            (user_ids[1], project_2),
+            (user_ids[0], project_2),
+            (user_ids[0], project_1),
         ],
     )
     .await
@@ -319,10 +274,10 @@ async fn test_user_activity() {
     db.record_user_activity(
         t4..t5,
         &[
-            (user_2, project_2),
-            (user_1, project_2),
-            (user_1, project_1),
-            (user_3, project_1),
+            (user_ids[1], project_2),
+            (user_ids[0], project_2),
+            (user_ids[0], project_1),
+            (user_ids[2], project_1),
         ],
     )
     .await
@@ -330,13 +285,16 @@ async fn test_user_activity() {
 
     // User 2 leaves
     let t6 = t5 + Duration::from_secs(5);
-    db.record_user_activity(t5..t6, &[(user_1, project_1), (user_3, project_1)])
-        .await
-        .unwrap();
+    db.record_user_activity(
+        t5..t6,
+        &[(user_ids[0], project_1), (user_ids[2], project_1)],
+    )
+    .await
+    .unwrap();
 
     let t7 = t6 + Duration::from_secs(60);
     let t8 = t7 + Duration::from_secs(10);
-    db.record_user_activity(t7..t8, &[(user_1, project_1)])
+    db.record_user_activity(t7..t8, &[(user_ids[0], project_1)])
         .await
         .unwrap();
 
@@ -344,8 +302,8 @@ async fn test_user_activity() {
         db.get_top_users_activity_summary(t0..t6, 10).await.unwrap(),
         &[
             UserActivitySummary {
-                id: user_1,
-                github_login: "u1".to_string(),
+                id: user_ids[0],
+                github_login: "user0".to_string(),
                 project_activity: vec![
                     ProjectActivitySummary {
                         id: project_1,
@@ -360,8 +318,8 @@ async fn test_user_activity() {
                 ]
             },
             UserActivitySummary {
-                id: user_2,
-                github_login: "u2".to_string(),
+                id: user_ids[1],
+                github_login: "user1".to_string(),
                 project_activity: vec![ProjectActivitySummary {
                     id: project_2,
                     duration: Duration::from_secs(50),
@@ -369,8 +327,8 @@ async fn test_user_activity() {
                 }]
             },
             UserActivitySummary {
-                id: user_3,
-                github_login: "u3".to_string(),
+                id: user_ids[2],
+                github_login: "user2".to_string(),
                 project_activity: vec![ProjectActivitySummary {
                     id: project_1,
                     duration: Duration::from_secs(15),
@@ -442,7 +400,9 @@ async fn test_user_activity() {
     );
 
     assert_eq!(
-        db.get_user_activity_timeline(t3..t6, user_1).await.unwrap(),
+        db.get_user_activity_timeline(t3..t6, user_ids[0])
+            .await
+            .unwrap(),
         &[
             UserActivityPeriod {
                 project_id: project_1,
@@ -459,7 +419,9 @@ async fn test_user_activity() {
         ]
     );
     assert_eq!(
-        db.get_user_activity_timeline(t0..t8, user_1).await.unwrap(),
+        db.get_user_activity_timeline(t0..t8, user_ids[0])
+            .await
+            .unwrap(),
         &[
             UserActivityPeriod {
                 project_id: project_2,
@@ -501,7 +463,8 @@ async fn test_recent_channel_messages() {
                 },
             )
             .await
-            .unwrap();
+            .unwrap()
+            .user_id;
         let org = db.create_org("org", "org").await.unwrap();
         let channel = db.create_org_channel(org, "channel").await.unwrap();
         for i in 0..10 {
@@ -545,7 +508,8 @@ async fn test_channel_message_nonces() {
                 },
             )
             .await
-            .unwrap();
+            .unwrap()
+            .user_id;
         let org = db.create_org("org", "org").await.unwrap();
         let channel = db.create_org_channel(org, "channel").await.unwrap();
 
@@ -587,7 +551,8 @@ async fn test_create_access_tokens() {
             },
         )
         .await
-        .unwrap();
+        .unwrap()
+        .user_id;
 
     db.create_access_token_hash(user, "h1", 3).await.unwrap();
     db.create_access_token_hash(user, "h2", 3).await.unwrap();
@@ -678,42 +643,27 @@ async fn test_add_contacts() {
     ] {
         let db = test_db.db();
 
-        let user_1 = db
-            .create_user(
-                "u1@example.com",
-                false,
-                NewUserParams {
-                    github_login: "u1".into(),
-                    github_user_id: 0,
-                    invite_count: 0,
-                },
-            )
-            .await
-            .unwrap();
-        let user_2 = db
-            .create_user(
-                "u2@example.com",
-                false,
-                NewUserParams {
-                    github_login: "u2".into(),
-                    github_user_id: 1,
-                    invite_count: 0,
-                },
-            )
-            .await
-            .unwrap();
-        let user_3 = db
-            .create_user(
-                "u3@example.com",
-                false,
-                NewUserParams {
-                    github_login: "u3".into(),
-                    github_user_id: 2,
-                    invite_count: 0,
-                },
-            )
-            .await
-            .unwrap();
+        let mut user_ids = Vec::new();
+        for i in 0..3 {
+            user_ids.push(
+                db.create_user(
+                    &format!("user{i}@example.com"),
+                    false,
+                    NewUserParams {
+                        github_login: format!("user{i}"),
+                        github_user_id: i,
+                        invite_count: 0,
+                    },
+                )
+                .await
+                .unwrap()
+                .user_id,
+            );
+        }
+
+        let user_1 = user_ids[0];
+        let user_2 = user_ids[1];
+        let user_3 = user_ids[2];
 
         // User starts with no contacts
         assert_eq!(
@@ -927,12 +877,12 @@ async fn test_add_contacts() {
 async fn test_invite_codes() {
     let postgres = TestDb::postgres().await;
     let db = postgres.db();
-    let user1 = db
+    let NewUserResult { user_id: user1, .. } = db
         .create_user(
-            "u1@example.com",
+            "user1@example.com",
             false,
             NewUserParams {
-                github_login: "u1".into(),
+                github_login: "user1".into(),
                 github_user_id: 0,
                 invite_count: 0,
             },
@@ -954,13 +904,14 @@ async fn test_invite_codes() {
 
     // User 2 redeems the invite code and becomes a contact of user 1.
     let user2_invite = db
-        .create_invite_from_code(&invite_code, "u2@example.com", Some("user-2-device-id"))
+        .create_invite_from_code(&invite_code, "user2@example.com", Some("user-2-device-id"))
         .await
         .unwrap();
     let NewUserResult {
         user_id: user2,
         inviting_user_id,
         signup_device_id,
+        metrics_id,
     } = db
         .create_user_from_invite(
             &user2_invite,
@@ -976,6 +927,7 @@ async fn test_invite_codes() {
     assert_eq!(invite_count, 1);
     assert_eq!(inviting_user_id, Some(user1));
     assert_eq!(signup_device_id.unwrap(), "user-2-device-id");
+    assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id);
     assert_eq!(
         db.get_contacts(user1).await.unwrap(),
         [
@@ -1009,13 +961,14 @@ async fn test_invite_codes() {
 
     // User 3 redeems the invite code and becomes a contact of user 1.
     let user3_invite = db
-        .create_invite_from_code(&invite_code, "u3@example.com", None)
+        .create_invite_from_code(&invite_code, "user3@example.com", None)
         .await
         .unwrap();
     let NewUserResult {
         user_id: user3,
         inviting_user_id,
         signup_device_id,
+        ..
     } = db
         .create_user_from_invite(
             &user3_invite,
@@ -1067,7 +1020,7 @@ async fn test_invite_codes() {
     );
 
     // Trying to reedem the code for the third time results in an error.
-    db.create_invite_from_code(&invite_code, "u4@example.com", Some("user-4-device-id"))
+    db.create_invite_from_code(&invite_code, "user4@example.com", Some("user-4-device-id"))
         .await
         .unwrap_err();
 
@@ -1079,7 +1032,7 @@ async fn test_invite_codes() {
 
     // User 4 can now redeem the invite code and becomes a contact of user 1.
     let user4_invite = db
-        .create_invite_from_code(&invite_code, "u4@example.com", Some("user-4-device-id"))
+        .create_invite_from_code(&invite_code, "user4@example.com", Some("user-4-device-id"))
         .await
         .unwrap();
     let user4 = db
@@ -1137,7 +1090,7 @@ async fn test_invite_codes() {
     );
 
     // An existing user cannot redeem invite codes.
-    db.create_invite_from_code(&invite_code, "u2@example.com", Some("user-2-device-id"))
+    db.create_invite_from_code(&invite_code, "user2@example.com", Some("user-2-device-id"))
         .await
         .unwrap_err();
     let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
@@ -1232,6 +1185,7 @@ async fn test_signups() {
         user_id,
         inviting_user_id,
         signup_device_id,
+        ..
     } = db
         .create_user_from_invite(
             &Invite {
@@ -1284,6 +1238,51 @@ async fn test_signups() {
     .unwrap_err();
 }
 
+#[tokio::test(flavor = "multi_thread")]
+async fn test_metrics_id() {
+    let postgres = TestDb::postgres().await;
+    let db = postgres.db();
+
+    let NewUserResult {
+        user_id: user1,
+        metrics_id: metrics_id1,
+        ..
+    } = db
+        .create_user(
+            "person1@example.com",
+            false,
+            NewUserParams {
+                github_login: "person1".into(),
+                github_user_id: 101,
+                invite_count: 5,
+            },
+        )
+        .await
+        .unwrap();
+    let NewUserResult {
+        user_id: user2,
+        metrics_id: metrics_id2,
+        ..
+    } = db
+        .create_user(
+            "person2@example.com",
+            false,
+            NewUserParams {
+                github_login: "person2".into(),
+                github_user_id: 102,
+                invite_count: 5,
+            },
+        )
+        .await
+        .unwrap();
+
+    assert_eq!(db.get_user_metrics_id(user1).await.unwrap(), metrics_id1);
+    assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id2);
+    assert_eq!(metrics_id1.len(), 36);
+    assert_eq!(metrics_id2.len(), 36);
+    assert_ne!(metrics_id1, metrics_id2);
+}
+
 fn build_background_executor() -> Arc<Background> {
     Deterministic::new(0).build_background()
 }

crates/collab/src/integration_tests.rs 🔗

@@ -4663,7 +4663,8 @@ async fn test_random_collaboration(
             },
         )
         .await
-        .unwrap();
+        .unwrap()
+        .user_id;
     let mut available_guests = vec![
         "guest-1".to_string(),
         "guest-2".to_string(),
@@ -4683,7 +4684,8 @@ async fn test_random_collaboration(
                 },
             )
             .await
-            .unwrap();
+            .unwrap()
+            .user_id;
         assert_eq!(*username, format!("guest-{}", guest_user_id));
         server
             .app_state
@@ -5206,6 +5208,7 @@ impl TestServer {
                 )
                 .await
                 .unwrap()
+                .user_id
         };
         let client_name = name.to_string();
         let mut client = cx.read(|cx| Client::new(http.clone(), cx));

crates/collab/src/rpc.rs 🔗

@@ -205,7 +205,8 @@ impl Server {
             .add_request_handler(Server::follow)
             .add_message_handler(Server::unfollow)
             .add_message_handler(Server::update_followers)
-            .add_request_handler(Server::get_channel_messages);
+            .add_request_handler(Server::get_channel_messages)
+            .add_request_handler(Server::get_private_user_info);
 
         Arc::new(server)
     }
@@ -1727,6 +1728,20 @@ impl Server {
         Ok(())
     }
 
+    async fn get_private_user_info(
+        self: Arc<Self>,
+        request: TypedEnvelope<proto::GetPrivateUserInfo>,
+        response: Response<proto::GetPrivateUserInfo>,
+    ) -> Result<()> {
+        let user_id = self
+            .store()
+            .await
+            .user_id_for_connection(request.sender_id)?;
+        let metrics_id = self.app_state.db.get_user_metrics_id(user_id).await?;
+        response.send(proto::GetPrivateUserInfoResponse { metrics_id })?;
+        Ok(())
+    }
+
     pub(crate) async fn store(&self) -> StoreGuard<'_> {
         #[cfg(test)]
         tokio::task::yield_now().await;

crates/contacts_panel/src/contacts_panel.rs 🔗

@@ -1220,6 +1220,17 @@ mod tests {
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
         let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake()));
         let server = FakeServer::for_client(current_user_id, &client, cx).await;
+
+        let request = server.receive::<proto::GetPrivateUserInfo>().await.unwrap();
+        server
+            .respond(
+                request.receipt(),
+                proto::GetPrivateUserInfoResponse {
+                    metrics_id: "the-metrics-id".into(),
+                },
+            )
+            .await;
+
         let fs = FakeFs::new(cx.background());
         fs.insert_tree("/private_dir", json!({ "one.rs": "" }))
             .await;

crates/rpc/proto/zed.proto 🔗

@@ -108,6 +108,9 @@ message Envelope {
         FollowResponse follow_response = 93;
         UpdateFollowers update_followers = 94;
         Unfollow unfollow = 95;
+
+        GetPrivateUserInfo get_private_user_info = 96;
+        GetPrivateUserInfoResponse get_private_user_info_response = 97;
     }
 }
 
@@ -748,6 +751,12 @@ message Unfollow {
     uint32 leader_id = 2;
 }
 
+message GetPrivateUserInfo {}
+
+message GetPrivateUserInfoResponse {
+    string metrics_id = 1;
+}
+
 // Entities
 
 message UpdateActiveView {

crates/rpc/src/proto.rs 🔗

@@ -167,6 +167,8 @@ messages!(
     (UpdateProject, Foreground),
     (UpdateWorktree, Foreground),
     (UpdateWorktreeExtensions, Background),
+    (GetPrivateUserInfo, Foreground),
+    (GetPrivateUserInfoResponse, Foreground),
 );
 
 request_messages!(
@@ -189,6 +191,7 @@ request_messages!(
     (GetTypeDefinition, GetTypeDefinitionResponse),
     (GetDocumentHighlights, GetDocumentHighlightsResponse),
     (GetReferences, GetReferencesResponse),
+    (GetPrivateUserInfo, GetPrivateUserInfoResponse),
     (GetProjectSymbols, GetProjectSymbolsResponse),
     (FuzzySearchUsers, UsersResponse),
     (GetUsers, UsersResponse),