Link signups to users in telemetry via a stored device_id

Co-authored-by: Joseph Lyons <joseph@zed.dev>
This commit is contained in:
Max Brunsfeld 2022-09-23 17:06:27 -07:00
parent 04baccbea6
commit 4784dbe498
7 changed files with 124 additions and 109 deletions

View file

@ -14,9 +14,11 @@ use async_tungstenite::tungstenite::{
}; };
use futures::{future::LocalBoxFuture, FutureExt, SinkExt, StreamExt, TryStreamExt}; use futures::{future::LocalBoxFuture, FutureExt, SinkExt, StreamExt, TryStreamExt};
use gpui::{ use gpui::{
actions, serde_json::Value, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, actions,
AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, serde_json::{json, Value},
MutableAppContext, Task, View, ViewContext, ViewHandle, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext,
AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext,
ViewHandle,
}; };
use http::HttpClient; use http::HttpClient;
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -52,13 +54,29 @@ lazy_static! {
pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894"; pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
actions!(client, [Authenticate]); actions!(client, [Authenticate, TestTelemetry]);
pub fn init(rpc: Arc<Client>, cx: &mut MutableAppContext) { pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
cx.add_global_action(move |_: &Authenticate, cx| { cx.add_global_action({
let rpc = rpc.clone(); let client = client.clone();
cx.spawn(|cx| async move { rpc.authenticate_and_connect(true, &cx).log_err().await }) move |_: &Authenticate, cx| {
let client = client.clone();
cx.spawn(
|cx| async move { client.authenticate_and_connect(true, &cx).log_err().await },
)
.detach(); .detach();
}
});
cx.add_global_action({
let client = client.clone();
move |_: &TestTelemetry, _| {
client.log_event(
"test_telemetry",
json!({
"test_property": "test_value"
}),
)
}
}); });
} }

View file

@ -1,4 +1,4 @@
use crate::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN}; use crate::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
use gpui::{ use gpui::{
executor::Background, executor::Background,
serde_json::{self, value::Map, Value}, serde_json::{self, value::Map, Value},
@ -22,7 +22,6 @@ pub struct Telemetry {
#[derive(Default)] #[derive(Default)]
struct TelemetryState { struct TelemetryState {
metrics_id: Option<i32>,
device_id: Option<String>, device_id: Option<String>,
app_version: Option<AppVersion>, app_version: Option<AppVersion>,
os_version: Option<AppVersion>, os_version: Option<AppVersion>,
@ -33,7 +32,6 @@ struct TelemetryState {
#[derive(Serialize)] #[derive(Serialize)]
struct RecordEventParams { struct RecordEventParams {
token: &'static str, token: &'static str,
metrics_id: Option<i32>,
device_id: Option<String>, device_id: Option<String>,
app_version: Option<String>, app_version: Option<String>,
os_version: Option<String>, os_version: Option<String>,
@ -48,8 +46,13 @@ struct Event {
properties: Option<Map<String, Value>>, properties: Option<Map<String, Value>>,
} }
const MAX_QUEUE_LEN: usize = 30; #[cfg(debug_assertions)]
const EVENTS_URI: &'static str = "https://zed.dev/api/telemetry"; const MAX_QUEUE_LEN: usize = 1;
#[cfg(not(debug_assertions))]
const MAX_QUEUE_LEN: usize = 10;
const EVENTS_URI: &'static str = "api/telemetry";
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30); const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30);
impl Telemetry { impl Telemetry {
@ -61,7 +64,6 @@ impl Telemetry {
state: Mutex::new(TelemetryState { state: Mutex::new(TelemetryState {
os_version: platform.os_version().log_err(), os_version: platform.os_version().log_err(),
app_version: platform.app_version().log_err(), app_version: platform.app_version().log_err(),
metrics_id: None,
device_id: None, device_id: None,
queue: Default::default(), queue: Default::default(),
flush_task: Default::default(), flush_task: Default::default(),
@ -69,10 +71,6 @@ impl Telemetry {
}) })
} }
pub fn set_metrics_id(&self, metrics_id: Option<i32>) {
self.state.lock().metrics_id = metrics_id;
}
pub fn log_event(self: &Arc<Self>, kind: &str, properties: Value) { pub fn log_event(self: &Arc<Self>, kind: &str, properties: Value) {
let mut state = self.state.lock(); let mut state = self.state.lock();
state.queue.push(Event { state.queue.push(Event {
@ -88,6 +86,7 @@ impl Telemetry {
}, },
}); });
if state.queue.len() >= MAX_QUEUE_LEN { if state.queue.len() >= MAX_QUEUE_LEN {
drop(state);
self.flush(); self.flush();
} else { } else {
let this = self.clone(); let this = self.clone();
@ -105,7 +104,6 @@ impl Telemetry {
let client = self.client.clone(); let client = self.client.clone();
let app_version = state.app_version; let app_version = state.app_version;
let os_version = state.os_version; let os_version = state.os_version;
let metrics_id = state.metrics_id;
let device_id = state.device_id.clone(); let device_id = state.device_id.clone();
state.flush_task.take(); state.flush_task.take();
self.executor self.executor
@ -115,11 +113,13 @@ impl Telemetry {
events, events,
app_version: app_version.map(|v| v.to_string()), app_version: app_version.map(|v| v.to_string()),
os_version: os_version.map(|v| v.to_string()), os_version: os_version.map(|v| v.to_string()),
metrics_id,
device_id, device_id,
}) })
.log_err()?; .log_err()?;
let request = Request::post(EVENTS_URI).body(body.into()).log_err()?; let request = Request::post(format!("{}/{}", *ZED_SERVER_URL, EVENTS_URI))
.header("Content-Type", "application/json")
.body(body.into())
.log_err()?;
client.send(request).await.log_err(); client.send(request).await.log_err();
Some(()) Some(())
}) })

View file

@ -1,9 +1,7 @@
DROP TABLE signups; DROP TABLE signups;
ALTER TABLE users ALTER TABLE users
DROP COLUMN github_user_id, DROP COLUMN github_user_id;
DROP COLUMN metrics_id;
DROP SEQUENCE metrics_id_seq;
DROP INDEX index_users_on_email_address; DROP INDEX index_users_on_email_address;
DROP INDEX index_users_on_github_user_id;

View file

@ -1,12 +1,10 @@
CREATE SEQUENCE metrics_id_seq;
CREATE TABLE IF NOT EXISTS "signups" ( CREATE TABLE IF NOT EXISTS "signups" (
"id" SERIAL PRIMARY KEY NOT NULL, "id" SERIAL PRIMARY KEY,
"email_address" VARCHAR NOT NULL, "email_address" VARCHAR NOT NULL,
"email_confirmation_code" VARCHAR(64) NOT NULL, "email_confirmation_code" VARCHAR(64) NOT NULL,
"email_confirmation_sent" BOOLEAN NOT NULL, "email_confirmation_sent" BOOLEAN NOT NULL,
"metrics_id" INTEGER NOT NULL DEFAULT nextval('metrics_id_seq'),
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"device_id" VARCHAR NOT NULL,
"user_id" INTEGER REFERENCES users (id) ON DELETE CASCADE, "user_id" INTEGER REFERENCES users (id) ON DELETE CASCADE,
"inviting_user_id" INTEGER REFERENCES users (id) ON DELETE SET NULL, "inviting_user_id" INTEGER REFERENCES users (id) ON DELETE SET NULL,
@ -23,11 +21,7 @@ CREATE UNIQUE INDEX "index_signups_on_email_address" ON "signups" ("email_addres
CREATE INDEX "index_signups_on_email_confirmation_sent" ON "signups" ("email_confirmation_sent"); CREATE INDEX "index_signups_on_email_confirmation_sent" ON "signups" ("email_confirmation_sent");
ALTER TABLE "users" ALTER TABLE "users"
ADD "github_user_id" INTEGER, ADD "github_user_id" INTEGER;
ADD "metrics_id" INTEGER DEFAULT nextval('metrics_id_seq');
CREATE INDEX "index_users_on_email_address" ON "users" ("email_address"); CREATE INDEX "index_users_on_email_address" ON "users" ("email_address");
CREATE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id"); CREATE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id");
UPDATE users
SET metrics_id = nextval('metrics_id_seq');

View file

@ -127,20 +127,28 @@ struct CreateUserParams {
invite_count: i32, invite_count: i32,
} }
#[derive(Serialize, Debug)]
struct CreateUserResponse {
user: User,
signup_device_id: Option<String>,
}
async fn create_user( async fn create_user(
Json(params): Json<CreateUserParams>, Json(params): Json<CreateUserParams>,
Extension(app): Extension<Arc<AppState>>, Extension(app): Extension<Arc<AppState>>,
Extension(rpc_server): Extension<Arc<rpc::Server>>, Extension(rpc_server): Extension<Arc<rpc::Server>>,
) -> Result<Json<User>> { ) -> Result<Json<CreateUserResponse>> {
let user = NewUserParams { let user = NewUserParams {
github_login: params.github_login, github_login: params.github_login,
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, inviter_id) = 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 { if let Some(email_confirmation_code) = params.email_confirmation_code {
app.db let result = app
.db
.create_user_from_invite( .create_user_from_invite(
&Invite { &Invite {
email_address: params.email_address, email_address: params.email_address,
@ -148,24 +156,24 @@ async fn create_user(
}, },
user, user,
) )
.await? .await?;
} user_id = result.0;
// Creating a user as an admin signup_device_id = Some(result.2);
else { if let Some(inviter_id) = result.1 {
(
app.db
.create_user(&params.email_address, false, user)
.await?,
None,
)
};
if let Some(inviter_id) = inviter_id {
rpc_server rpc_server
.invite_code_redeemed(inviter_id, user_id) .invite_code_redeemed(inviter_id, user_id)
.await .await
.trace_err(); .trace_err();
} }
}
// Creating a user as an admin
else {
user_id = app
.db
.create_user(&params.email_address, false, user)
.await?;
signup_device_id = None;
}
let user = app let user = app
.db .db
@ -173,7 +181,10 @@ async fn create_user(
.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(user)) Ok(Json(CreateUserResponse {
user,
signup_device_id,
}))
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -396,17 +407,12 @@ async fn get_user_for_invite_code(
Ok(Json(app.db.get_user_for_invite_code(&code).await?)) Ok(Json(app.db.get_user_for_invite_code(&code).await?))
} }
#[derive(Serialize)]
struct CreateSignupResponse {
metrics_id: i32,
}
async fn create_signup( async fn create_signup(
Json(params): Json<Signup>, Json(params): Json<Signup>,
Extension(app): Extension<Arc<AppState>>, Extension(app): Extension<Arc<AppState>>,
) -> Result<Json<CreateSignupResponse>> { ) -> Result<()> {
let metrics_id = app.db.create_signup(params).await?; app.db.create_signup(params).await?;
Ok(Json(CreateSignupResponse { metrics_id })) Ok(())
} }
async fn get_waitlist_summary( async fn get_waitlist_summary(

View file

@ -37,7 +37,7 @@ pub trait Db: Send + Sync {
async fn get_user_for_invite_code(&self, code: &str) -> Result<User>; async fn get_user_for_invite_code(&self, code: &str) -> Result<User>;
async fn create_invite_from_code(&self, code: &str, email_address: &str) -> Result<Invite>; async fn create_invite_from_code(&self, code: &str, email_address: &str) -> Result<Invite>;
async fn create_signup(&self, signup: Signup) -> Result<i32>; async fn create_signup(&self, signup: Signup) -> Result<()>;
async fn get_waitlist_summary(&self) -> Result<WaitlistSummary>; async fn get_waitlist_summary(&self) -> Result<WaitlistSummary>;
async fn get_unsent_invites(&self, count: usize) -> Result<Vec<Invite>>; async fn get_unsent_invites(&self, count: usize) -> Result<Vec<Invite>>;
async fn record_sent_invites(&self, invites: &[Invite]) -> Result<()>; async fn record_sent_invites(&self, invites: &[Invite]) -> Result<()>;
@ -45,7 +45,7 @@ pub trait Db: Send + Sync {
&self, &self,
invite: &Invite, invite: &Invite,
user: NewUserParams, user: NewUserParams,
) -> Result<(UserId, Option<UserId>)>; ) -> Result<(UserId, Option<UserId>, String)>;
/// Registers a new project for the given user. /// Registers a new project for the given user.
async fn register_project(&self, host_user_id: UserId) -> Result<ProjectId>; async fn register_project(&self, host_user_id: UserId) -> Result<ProjectId>;
@ -364,8 +364,8 @@ impl Db for PostgresDb {
// signups // signups
async fn create_signup(&self, signup: Signup) -> Result<i32> { async fn create_signup(&self, signup: Signup) -> Result<()> {
Ok(sqlx::query_scalar( sqlx::query(
" "
INSERT INTO signups INSERT INTO signups
( (
@ -377,10 +377,11 @@ impl Db for PostgresDb {
platform_windows, platform_windows,
platform_unknown, platform_unknown,
editor_features, editor_features,
programming_languages programming_languages,
device_id
) )
VALUES VALUES
($1, $2, 'f', $3, $4, $5, 'f', $6, $7) ($1, $2, 'f', $3, $4, $5, 'f', $6, $7, $8)
RETURNING id RETURNING id
", ",
) )
@ -391,8 +392,10 @@ impl Db for PostgresDb {
.bind(&signup.platform_windows) .bind(&signup.platform_windows)
.bind(&signup.editor_features) .bind(&signup.editor_features)
.bind(&signup.programming_languages) .bind(&signup.programming_languages)
.fetch_one(&self.pool) .bind(&signup.device_id)
.await?) .execute(&self.pool)
.await?;
Ok(())
} }
async fn get_waitlist_summary(&self) -> Result<WaitlistSummary> { async fn get_waitlist_summary(&self) -> Result<WaitlistSummary> {
@ -455,17 +458,17 @@ impl Db for PostgresDb {
&self, &self,
invite: &Invite, invite: &Invite,
user: NewUserParams, user: NewUserParams,
) -> Result<(UserId, Option<UserId>)> { ) -> Result<(UserId, Option<UserId>, String)> {
let mut tx = self.pool.begin().await?; let mut tx = self.pool.begin().await?;
let (signup_id, metrics_id, existing_user_id, inviting_user_id): ( let (signup_id, existing_user_id, inviting_user_id, device_id): (
i32,
i32, i32,
Option<UserId>, Option<UserId>,
Option<UserId>, Option<UserId>,
String,
) = sqlx::query_as( ) = sqlx::query_as(
" "
SELECT id, metrics_id, user_id, inviting_user_id SELECT id, user_id, inviting_user_id, device_id
FROM signups FROM signups
WHERE WHERE
email_address = $1 AND email_address = $1 AND
@ -488,9 +491,9 @@ impl Db for PostgresDb {
let user_id: UserId = sqlx::query_scalar( let user_id: UserId = sqlx::query_scalar(
" "
INSERT INTO users INSERT INTO users
(email_address, github_login, github_user_id, admin, invite_count, invite_code, metrics_id) (email_address, github_login, github_user_id, admin, invite_count, invite_code)
VALUES VALUES
($1, $2, $3, 'f', $4, $5, $6) ($1, $2, $3, 'f', $4, $5)
RETURNING id RETURNING id
", ",
) )
@ -499,7 +502,6 @@ impl Db for PostgresDb {
.bind(&user.github_user_id) .bind(&user.github_user_id)
.bind(&user.invite_count) .bind(&user.invite_count)
.bind(random_invite_code()) .bind(random_invite_code())
.bind(metrics_id)
.fetch_one(&mut tx) .fetch_one(&mut tx)
.await?; .await?;
@ -550,7 +552,7 @@ impl Db for PostgresDb {
} }
tx.commit().await?; tx.commit().await?;
Ok((user_id, inviting_user_id)) Ok((user_id, inviting_user_id, device_id))
} }
// invite codes // invite codes
@ -1567,7 +1569,6 @@ pub struct User {
pub id: UserId, pub id: UserId,
pub github_login: String, pub github_login: String,
pub github_user_id: Option<i32>, pub github_user_id: Option<i32>,
pub metrics_id: i32,
pub email_address: Option<String>, pub email_address: Option<String>,
pub admin: bool, pub admin: bool,
pub invite_code: Option<String>, pub invite_code: Option<String>,
@ -1674,6 +1675,7 @@ pub struct Signup {
pub platform_linux: bool, pub platform_linux: bool,
pub editor_features: Vec<String>, pub editor_features: Vec<String>,
pub programming_languages: Vec<String>, pub programming_languages: Vec<String>,
pub device_id: String,
} }
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, FromRow)] #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, FromRow)]
@ -1802,7 +1804,6 @@ mod test {
github_login: params.github_login, github_login: params.github_login,
github_user_id: Some(params.github_user_id), github_user_id: Some(params.github_user_id),
email_address: Some(email_address.to_string()), email_address: Some(email_address.to_string()),
metrics_id: id + 100,
admin, admin,
invite_code: None, invite_code: None,
invite_count: 0, invite_count: 0,
@ -1884,7 +1885,7 @@ mod test {
// signups // signups
async fn create_signup(&self, _signup: Signup) -> Result<i32> { async fn create_signup(&self, _signup: Signup) -> Result<()> {
unimplemented!() unimplemented!()
} }
@ -1904,7 +1905,7 @@ mod test {
&self, &self,
_invite: &Invite, _invite: &Invite,
_user: NewUserParams, _user: NewUserParams,
) -> Result<(UserId, Option<UserId>)> { ) -> Result<(UserId, Option<UserId>, String)> {
unimplemented!() unimplemented!()
} }

View file

@ -957,7 +957,7 @@ async fn test_invite_codes() {
.create_invite_from_code(&invite_code, "u2@example.com") .create_invite_from_code(&invite_code, "u2@example.com")
.await .await
.unwrap(); .unwrap();
let (user2, inviter) = db let (user2, inviter, _) = db
.create_user_from_invite( .create_user_from_invite(
&user2_invite, &user2_invite,
NewUserParams { NewUserParams {
@ -1007,7 +1007,7 @@ async fn test_invite_codes() {
.create_invite_from_code(&invite_code, "u3@example.com") .create_invite_from_code(&invite_code, "u3@example.com")
.await .await
.unwrap(); .unwrap();
let (user3, inviter) = db let (user3, inviter, _) = db
.create_user_from_invite( .create_user_from_invite(
&user3_invite, &user3_invite,
NewUserParams { NewUserParams {
@ -1072,7 +1072,7 @@ async fn test_invite_codes() {
.create_invite_from_code(&invite_code, "u4@example.com") .create_invite_from_code(&invite_code, "u4@example.com")
.await .await
.unwrap(); .unwrap();
let (user4, _) = db let (user4, _, _) = db
.create_user_from_invite( .create_user_from_invite(
&user4_invite, &user4_invite,
NewUserParams { NewUserParams {
@ -1139,9 +1139,7 @@ async fn test_signups() {
let db = postgres.db(); let db = postgres.db();
// people sign up on the waitlist // people sign up on the waitlist
let mut signup_metric_ids = Vec::new();
for i in 0..8 { for i in 0..8 {
signup_metric_ids.push(
db.create_signup(Signup { db.create_signup(Signup {
email_address: format!("person-{i}@example.com"), email_address: format!("person-{i}@example.com"),
platform_mac: true, platform_mac: true,
@ -1149,10 +1147,10 @@ async fn test_signups() {
platform_windows: i % 4 == 0, platform_windows: i % 4 == 0,
editor_features: vec!["speed".into()], editor_features: vec!["speed".into()],
programming_languages: vec!["rust".into(), "c".into()], programming_languages: vec!["rust".into(), "c".into()],
device_id: format!("device_id_{i}"),
}) })
.await .await
.unwrap(), .unwrap();
);
} }
assert_eq!( assert_eq!(
@ -1219,7 +1217,7 @@ async fn test_signups() {
// user completes the signup process by providing their // user completes the signup process by providing their
// github account. // github account.
let (user_id, inviter_id) = db let (user_id, inviter_id, signup_device_id) = db
.create_user_from_invite( .create_user_from_invite(
&Invite { &Invite {
email_address: signups_batch1[0].email_address.clone(), email_address: signups_batch1[0].email_address.clone(),
@ -1238,7 +1236,7 @@ async fn test_signups() {
assert_eq!(user.github_login, "person-0"); assert_eq!(user.github_login, "person-0");
assert_eq!(user.email_address.as_deref(), Some("person-0@example.com")); assert_eq!(user.email_address.as_deref(), Some("person-0@example.com"));
assert_eq!(user.invite_count, 5); assert_eq!(user.invite_count, 5);
assert_eq!(user.metrics_id, signup_metric_ids[0]); assert_eq!(signup_device_id, "device_id_0");
// cannot redeem the same signup again. // cannot redeem the same signup again.
db.create_user_from_invite( db.create_user_from_invite(