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 gpui::{
actions, serde_json::Value, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle,
AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
MutableAppContext, Task, View, ViewContext, ViewHandle,
actions,
serde_json::{json, Value},
AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext,
AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext,
ViewHandle,
};
use http::HttpClient;
use lazy_static::lazy_static;
@ -52,13 +54,29 @@ lazy_static! {
pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
actions!(client, [Authenticate]);
actions!(client, [Authenticate, TestTelemetry]);
pub fn init(rpc: Arc<Client>, cx: &mut MutableAppContext) {
cx.add_global_action(move |_: &Authenticate, cx| {
let rpc = rpc.clone();
cx.spawn(|cx| async move { rpc.authenticate_and_connect(true, &cx).log_err().await })
pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
cx.add_global_action({
let client = client.clone();
move |_: &Authenticate, cx| {
let client = client.clone();
cx.spawn(
|cx| async move { client.authenticate_and_connect(true, &cx).log_err().await },
)
.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::{
executor::Background,
serde_json::{self, value::Map, Value},
@ -22,7 +22,6 @@ pub struct Telemetry {
#[derive(Default)]
struct TelemetryState {
metrics_id: Option<i32>,
device_id: Option<String>,
app_version: Option<AppVersion>,
os_version: Option<AppVersion>,
@ -33,7 +32,6 @@ struct TelemetryState {
#[derive(Serialize)]
struct RecordEventParams {
token: &'static str,
metrics_id: Option<i32>,
device_id: Option<String>,
app_version: Option<String>,
os_version: Option<String>,
@ -48,8 +46,13 @@ struct Event {
properties: Option<Map<String, Value>>,
}
const MAX_QUEUE_LEN: usize = 30;
const EVENTS_URI: &'static str = "https://zed.dev/api/telemetry";
#[cfg(debug_assertions)]
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);
impl Telemetry {
@ -61,7 +64,6 @@ impl Telemetry {
state: Mutex::new(TelemetryState {
os_version: platform.os_version().log_err(),
app_version: platform.app_version().log_err(),
metrics_id: None,
device_id: None,
queue: 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) {
let mut state = self.state.lock();
state.queue.push(Event {
@ -88,6 +86,7 @@ impl Telemetry {
},
});
if state.queue.len() >= MAX_QUEUE_LEN {
drop(state);
self.flush();
} else {
let this = self.clone();
@ -105,7 +104,6 @@ impl Telemetry {
let client = self.client.clone();
let app_version = state.app_version;
let os_version = state.os_version;
let metrics_id = state.metrics_id;
let device_id = state.device_id.clone();
state.flush_task.take();
self.executor
@ -115,11 +113,13 @@ impl Telemetry {
events,
app_version: app_version.map(|v| v.to_string()),
os_version: os_version.map(|v| v.to_string()),
metrics_id,
device_id,
})
.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();
Some(())
})

View File

@ -1,9 +1,7 @@
DROP TABLE signups;
ALTER TABLE users
DROP COLUMN github_user_id,
DROP COLUMN metrics_id;
DROP SEQUENCE metrics_id_seq;
DROP COLUMN github_user_id;
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" (
"id" SERIAL PRIMARY KEY NOT NULL,
"id" SERIAL PRIMARY KEY,
"email_address" VARCHAR NOT NULL,
"email_confirmation_code" VARCHAR(64) 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,
"device_id" VARCHAR NOT NULL,
"user_id" INTEGER REFERENCES users (id) ON DELETE CASCADE,
"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");
ALTER TABLE "users"
ADD "github_user_id" INTEGER,
ADD "metrics_id" INTEGER DEFAULT nextval('metrics_id_seq');
ADD "github_user_id" INTEGER;
CREATE INDEX "index_users_on_email_address" ON "users" ("email_address");
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,44 +127,52 @@ struct CreateUserParams {
invite_count: i32,
}
#[derive(Serialize, Debug)]
struct CreateUserResponse {
user: User,
signup_device_id: Option<String>,
}
async fn create_user(
Json(params): Json<CreateUserParams>,
Extension(app): Extension<Arc<AppState>>,
Extension(rpc_server): Extension<Arc<rpc::Server>>,
) -> Result<Json<User>> {
) -> Result<Json<CreateUserResponse>> {
let user = NewUserParams {
github_login: params.github_login,
github_user_id: params.github_user_id,
invite_count: params.invite_count,
};
let (user_id, inviter_id) =
// Creating a user via the normal signup process
if let Some(email_confirmation_code) = params.email_confirmation_code {
app.db
.create_user_from_invite(
&Invite {
email_address: params.email_address,
email_confirmation_code,
},
user,
)
.await?
}
// Creating a user as an admin
else {
(
app.db
.create_user(&params.email_address, false, user)
.await?,
None,
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
.create_user_from_invite(
&Invite {
email_address: params.email_address,
email_confirmation_code,
},
user,
)
};
if let Some(inviter_id) = inviter_id {
rpc_server
.invite_code_redeemed(inviter_id, user_id)
.await
.trace_err();
.await?;
user_id = result.0;
signup_device_id = Some(result.2);
if let Some(inviter_id) = result.1 {
rpc_server
.invite_code_redeemed(inviter_id, user_id)
.await
.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
@ -173,7 +181,10 @@ async fn create_user(
.await?
.ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
Ok(Json(user))
Ok(Json(CreateUserResponse {
user,
signup_device_id,
}))
}
#[derive(Deserialize)]
@ -396,17 +407,12 @@ async fn get_user_for_invite_code(
Ok(Json(app.db.get_user_for_invite_code(&code).await?))
}
#[derive(Serialize)]
struct CreateSignupResponse {
metrics_id: i32,
}
async fn create_signup(
Json(params): Json<Signup>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<Json<CreateSignupResponse>> {
let metrics_id = app.db.create_signup(params).await?;
Ok(Json(CreateSignupResponse { metrics_id }))
) -> Result<()> {
app.db.create_signup(params).await?;
Ok(())
}
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 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_unsent_invites(&self, count: usize) -> Result<Vec<Invite>>;
async fn record_sent_invites(&self, invites: &[Invite]) -> Result<()>;
@ -45,7 +45,7 @@ pub trait Db: Send + Sync {
&self,
invite: &Invite,
user: NewUserParams,
) -> Result<(UserId, Option<UserId>)>;
) -> Result<(UserId, Option<UserId>, String)>;
/// Registers a new project for the given user.
async fn register_project(&self, host_user_id: UserId) -> Result<ProjectId>;
@ -364,8 +364,8 @@ impl Db for PostgresDb {
// signups
async fn create_signup(&self, signup: Signup) -> Result<i32> {
Ok(sqlx::query_scalar(
async fn create_signup(&self, signup: Signup) -> Result<()> {
sqlx::query(
"
INSERT INTO signups
(
@ -377,10 +377,11 @@ impl Db for PostgresDb {
platform_windows,
platform_unknown,
editor_features,
programming_languages
programming_languages,
device_id
)
VALUES
($1, $2, 'f', $3, $4, $5, 'f', $6, $7)
($1, $2, 'f', $3, $4, $5, 'f', $6, $7, $8)
RETURNING id
",
)
@ -391,8 +392,10 @@ impl Db for PostgresDb {
.bind(&signup.platform_windows)
.bind(&signup.editor_features)
.bind(&signup.programming_languages)
.fetch_one(&self.pool)
.await?)
.bind(&signup.device_id)
.execute(&self.pool)
.await?;
Ok(())
}
async fn get_waitlist_summary(&self) -> Result<WaitlistSummary> {
@ -455,17 +458,17 @@ impl Db for PostgresDb {
&self,
invite: &Invite,
user: NewUserParams,
) -> Result<(UserId, Option<UserId>)> {
) -> Result<(UserId, Option<UserId>, String)> {
let mut tx = self.pool.begin().await?;
let (signup_id, metrics_id, existing_user_id, inviting_user_id): (
i32,
let (signup_id, existing_user_id, inviting_user_id, device_id): (
i32,
Option<UserId>,
Option<UserId>,
String,
) = sqlx::query_as(
"
SELECT id, metrics_id, user_id, inviting_user_id
SELECT id, user_id, inviting_user_id, device_id
FROM signups
WHERE
email_address = $1 AND
@ -488,9 +491,9 @@ impl Db for PostgresDb {
let user_id: UserId = sqlx::query_scalar(
"
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
($1, $2, $3, 'f', $4, $5, $6)
($1, $2, $3, 'f', $4, $5)
RETURNING id
",
)
@ -499,7 +502,6 @@ impl Db for PostgresDb {
.bind(&user.github_user_id)
.bind(&user.invite_count)
.bind(random_invite_code())
.bind(metrics_id)
.fetch_one(&mut tx)
.await?;
@ -550,7 +552,7 @@ impl Db for PostgresDb {
}
tx.commit().await?;
Ok((user_id, inviting_user_id))
Ok((user_id, inviting_user_id, device_id))
}
// invite codes
@ -1567,7 +1569,6 @@ pub struct User {
pub id: UserId,
pub github_login: String,
pub github_user_id: Option<i32>,
pub metrics_id: i32,
pub email_address: Option<String>,
pub admin: bool,
pub invite_code: Option<String>,
@ -1674,6 +1675,7 @@ pub struct Signup {
pub platform_linux: bool,
pub editor_features: Vec<String>,
pub programming_languages: Vec<String>,
pub device_id: String,
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, FromRow)]
@ -1802,7 +1804,6 @@ mod test {
github_login: params.github_login,
github_user_id: Some(params.github_user_id),
email_address: Some(email_address.to_string()),
metrics_id: id + 100,
admin,
invite_code: None,
invite_count: 0,
@ -1884,7 +1885,7 @@ mod test {
// signups
async fn create_signup(&self, _signup: Signup) -> Result<i32> {
async fn create_signup(&self, _signup: Signup) -> Result<()> {
unimplemented!()
}
@ -1904,7 +1905,7 @@ mod test {
&self,
_invite: &Invite,
_user: NewUserParams,
) -> Result<(UserId, Option<UserId>)> {
) -> Result<(UserId, Option<UserId>, String)> {
unimplemented!()
}

View File

@ -957,7 +957,7 @@ async fn test_invite_codes() {
.create_invite_from_code(&invite_code, "u2@example.com")
.await
.unwrap();
let (user2, inviter) = db
let (user2, inviter, _) = db
.create_user_from_invite(
&user2_invite,
NewUserParams {
@ -1007,7 +1007,7 @@ async fn test_invite_codes() {
.create_invite_from_code(&invite_code, "u3@example.com")
.await
.unwrap();
let (user3, inviter) = db
let (user3, inviter, _) = db
.create_user_from_invite(
&user3_invite,
NewUserParams {
@ -1072,7 +1072,7 @@ async fn test_invite_codes() {
.create_invite_from_code(&invite_code, "u4@example.com")
.await
.unwrap();
let (user4, _) = db
let (user4, _, _) = db
.create_user_from_invite(
&user4_invite,
NewUserParams {
@ -1139,20 +1139,18 @@ async fn test_signups() {
let db = postgres.db();
// people sign up on the waitlist
let mut signup_metric_ids = Vec::new();
for i in 0..8 {
signup_metric_ids.push(
db.create_signup(Signup {
email_address: format!("person-{i}@example.com"),
platform_mac: true,
platform_linux: i % 2 == 0,
platform_windows: i % 4 == 0,
editor_features: vec!["speed".into()],
programming_languages: vec!["rust".into(), "c".into()],
})
.await
.unwrap(),
);
db.create_signup(Signup {
email_address: format!("person-{i}@example.com"),
platform_mac: true,
platform_linux: i % 2 == 0,
platform_windows: i % 4 == 0,
editor_features: vec!["speed".into()],
programming_languages: vec!["rust".into(), "c".into()],
device_id: format!("device_id_{i}"),
})
.await
.unwrap();
}
assert_eq!(
@ -1219,7 +1217,7 @@ async fn test_signups() {
// user completes the signup process by providing their
// github account.
let (user_id, inviter_id) = db
let (user_id, inviter_id, signup_device_id) = db
.create_user_from_invite(
&Invite {
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.email_address.as_deref(), Some("person-0@example.com"));
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.
db.create_user_from_invite(