Identify users in amplitude via a separate 'metrics_id' UUID

This commit is contained in:
Max Brunsfeld 2022-09-29 12:32:25 -07:00
parent efdedaab53
commit 5d09083a7d
13 changed files with 316 additions and 239 deletions

View file

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

View file

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

View file

@ -142,10 +142,14 @@ impl UserStore {
match status { match status {
Status::Connected { .. } => { Status::Connected { .. } => {
if let Some((this, user_id)) = this.upgrade(&cx).zip(client.user_id()) { 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)) .update(&mut cx, |this, cx| this.fetch_user(user_id, cx))
.log_err() .log_err();
.await; 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(); current_user_tx.send(user).await.ok();
} }
} }

View file

@ -1,6 +0,0 @@
DROP TABLE signups;
ALTER TABLE users
DROP COLUMN github_user_id;
DROP INDEX index_users_on_email_address;

View file

@ -0,0 +1,2 @@
ALTER TABLE "users"
ADD "metrics_id" uuid NOT NULL DEFAULT gen_random_uuid();

View file

@ -24,6 +24,7 @@ use tracing::instrument;
pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> { pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> {
Router::new() Router::new()
.route("/user", get(get_authenticated_user))
.route("/users", get(get_users).post(create_user)) .route("/users", get(get_users).post(create_user))
.route("/users/:id", put(update_user).delete(destroy_user)) .route("/users/:id", put(update_user).delete(destroy_user))
.route("/users/:id/access_tokens", post(create_access_token)) .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) 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)] #[derive(Debug, Deserialize)]
struct GetUsersQueryParams { struct GetUsersQueryParams {
github_user_id: Option<i32>,
github_login: Option<String>,
query: Option<String>, query: Option<String>,
page: Option<u32>, page: Option<u32>,
limit: Option<u32>, limit: Option<u32>,
@ -98,14 +122,6 @@ async fn get_users(
Query(params): Query<GetUsersQueryParams>, Query(params): Query<GetUsersQueryParams>,
Extension(app): Extension<Arc<AppState>>, Extension(app): Extension<Arc<AppState>>,
) -> Result<Json<Vec<User>>> { ) -> 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 limit = params.limit.unwrap_or(100);
let users = if let Some(query) = params.query { let users = if let Some(query) = params.query {
app.db.fuzzy_search_users(&query, limit).await? app.db.fuzzy_search_users(&query, limit).await?
@ -124,6 +140,8 @@ struct CreateUserParams {
email_address: String, email_address: String,
email_confirmation_code: Option<String>, email_confirmation_code: Option<String>,
#[serde(default)] #[serde(default)]
admin: bool,
#[serde(default)]
invite_count: i32, invite_count: i32,
} }
@ -131,6 +149,7 @@ struct CreateUserParams {
struct CreateUserResponse { struct CreateUserResponse {
user: User, user: User,
signup_device_id: Option<String>, signup_device_id: Option<String>,
metrics_id: String,
} }
async fn create_user( async fn create_user(
@ -143,12 +162,10 @@ async fn create_user(
github_user_id: params.github_user_id, github_user_id: params.github_user_id,
invite_count: params.invite_count, invite_count: params.invite_count,
}; };
let user_id;
let signup_device_id;
// Creating a user via the normal signup process // Creating a user via the normal signup process
if let Some(email_confirmation_code) = params.email_confirmation_code { let result = if let Some(email_confirmation_code) = params.email_confirmation_code {
let result = app app.db
.db
.create_user_from_invite( .create_user_from_invite(
&Invite { &Invite {
email_address: params.email_address, email_address: params.email_address,
@ -156,34 +173,37 @@ async fn create_user(
}, },
user, user,
) )
.await?; .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();
}
} }
// Creating a user as an admin // Creating a user as an admin
else { else if params.admin {
user_id = app app.db
.db
.create_user(&params.email_address, false, user) .create_user(&params.email_address, false, user)
.await?; .await?
signup_device_id = None; } 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 let user = app
.db .db
.get_user_by_id(user_id) .get_user_by_id(result.user_id)
.await? .await?
.ok_or_else(|| anyhow!("couldn't find the user we just created"))?; .ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
Ok(Json(CreateUserResponse { Ok(Json(CreateUserResponse {
user, user,
signup_device_id, metrics_id: result.metrics_id,
signup_device_id: result.signup_device_id,
})) }))
} }

View file

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

View file

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

View file

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

View file

@ -205,7 +205,8 @@ impl Server {
.add_request_handler(Server::follow) .add_request_handler(Server::follow)
.add_message_handler(Server::unfollow) .add_message_handler(Server::unfollow)
.add_message_handler(Server::update_followers) .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) Arc::new(server)
} }
@ -1727,6 +1728,20 @@ impl Server {
Ok(()) 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<'_> { pub(crate) async fn store(&self) -> StoreGuard<'_> {
#[cfg(test)] #[cfg(test)]
tokio::task::yield_now().await; tokio::task::yield_now().await;

View file

@ -108,6 +108,9 @@ message Envelope {
FollowResponse follow_response = 93; FollowResponse follow_response = 93;
UpdateFollowers update_followers = 94; UpdateFollowers update_followers = 94;
Unfollow unfollow = 95; 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; uint32 leader_id = 2;
} }
message GetPrivateUserInfo {}
message GetPrivateUserInfoResponse {
string metrics_id = 1;
}
// Entities // Entities
message UpdateActiveView { message UpdateActiveView {

View file

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