mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-26 10:40:54 +00:00
Link signups to users in telemetry via a stored device_id
Co-authored-by: Joseph Lyons <joseph@zed.dev>
This commit is contained in:
parent
04baccbea6
commit
4784dbe498
7 changed files with 124 additions and 109 deletions
|
@ -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"
|
||||
}),
|
||||
)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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(())
|
||||
})
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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(¶ms.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(¶ms.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(
|
||||
|
|
|
@ -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!()
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue