Merge pull request #1635 from zed-industries/new-signup-flow
Implement APIs for new signup flow
This commit is contained in:
commit
5d8fe33bd2
31 changed files with 2571 additions and 1287 deletions
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
@ -56,6 +56,7 @@ jobs:
|
||||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||||
|
ZED_AMPLITUDE_API_KEY: ${{ secrets.ZED_AMPLITUDE_API_KEY }}
|
||||||
steps:
|
steps:
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
run: |
|
run: |
|
||||||
|
|
21
Cargo.lock
generated
21
Cargo.lock
generated
|
@ -945,6 +945,7 @@ dependencies = [
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
"async-tungstenite",
|
"async-tungstenite",
|
||||||
"collections",
|
"collections",
|
||||||
|
"db",
|
||||||
"futures",
|
"futures",
|
||||||
"gpui",
|
"gpui",
|
||||||
"image",
|
"image",
|
||||||
|
@ -955,13 +956,16 @@ dependencies = [
|
||||||
"postage",
|
"postage",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"rpc",
|
"rpc",
|
||||||
|
"serde",
|
||||||
"smol",
|
"smol",
|
||||||
"sum_tree",
|
"sum_tree",
|
||||||
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"time 0.3.11",
|
"time 0.3.11",
|
||||||
"tiny_http",
|
"tiny_http",
|
||||||
"url",
|
"url",
|
||||||
"util",
|
"util",
|
||||||
|
"uuid 1.1.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1503,6 +1507,19 @@ dependencies = [
|
||||||
"matches",
|
"matches",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "db"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
|
"collections",
|
||||||
|
"gpui",
|
||||||
|
"parking_lot 0.11.2",
|
||||||
|
"rocksdb",
|
||||||
|
"tempdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deflate"
|
name = "deflate"
|
||||||
version = "0.8.6"
|
version = "0.8.6"
|
||||||
|
@ -3949,6 +3966,7 @@ dependencies = [
|
||||||
"client",
|
"client",
|
||||||
"clock",
|
"clock",
|
||||||
"collections",
|
"collections",
|
||||||
|
"db",
|
||||||
"fsevent",
|
"fsevent",
|
||||||
"futures",
|
"futures",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
|
@ -6334,6 +6352,9 @@ name = "uuid"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f"
|
checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.7",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
|
|
|
@ -12,6 +12,7 @@ test-support = ["collections/test-support", "gpui/test-support", "rpc/test-suppo
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
|
db = { path = "../db" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
rpc = { path = "../rpc" }
|
rpc = { path = "../rpc" }
|
||||||
|
@ -31,7 +32,10 @@ smol = "1.2.5"
|
||||||
thiserror = "1.0.29"
|
thiserror = "1.0.29"
|
||||||
time = { version = "0.3", features = ["serde", "serde-well-known"] }
|
time = { version = "0.3", features = ["serde", "serde-well-known"] }
|
||||||
tiny_http = "0.8"
|
tiny_http = "0.8"
|
||||||
|
uuid = { version = "1.1.2", features = ["v4"] }
|
||||||
url = "2.2"
|
url = "2.2"
|
||||||
|
serde = { version = "*", features = ["derive"] }
|
||||||
|
tempfile = "3"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
collections = { path = "../collections", features = ["test-support"] }
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
|
|
|
@ -601,7 +601,7 @@ mod tests {
|
||||||
|
|
||||||
let user_id = 5;
|
let user_id = 5;
|
||||||
let http_client = FakeHttpClient::with_404_response();
|
let http_client = FakeHttpClient::with_404_response();
|
||||||
let client = Client::new(http_client.clone());
|
let client = cx.update(|cx| Client::new(http_client.clone(), cx));
|
||||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||||
|
|
||||||
Channel::init(&client);
|
Channel::init(&client);
|
||||||
|
|
|
@ -3,6 +3,7 @@ pub mod test;
|
||||||
|
|
||||||
pub mod channel;
|
pub mod channel;
|
||||||
pub mod http;
|
pub mod http;
|
||||||
|
pub mod telemetry;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
@ -11,10 +12,14 @@ use async_tungstenite::tungstenite::{
|
||||||
error::Error as WebsocketError,
|
error::Error as WebsocketError,
|
||||||
http::{Request, StatusCode},
|
http::{Request, StatusCode},
|
||||||
};
|
};
|
||||||
|
use db::Db;
|
||||||
use futures::{future::LocalBoxFuture, FutureExt, SinkExt, StreamExt, TryStreamExt};
|
use futures::{future::LocalBoxFuture, FutureExt, SinkExt, StreamExt, TryStreamExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AsyncAppContext,
|
actions,
|
||||||
Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle,
|
serde_json::{json, Value},
|
||||||
|
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;
|
||||||
|
@ -28,9 +33,11 @@ use std::{
|
||||||
convert::TryFrom,
|
convert::TryFrom,
|
||||||
fmt::Write as _,
|
fmt::Write as _,
|
||||||
future::Future,
|
future::Future,
|
||||||
|
path::PathBuf,
|
||||||
sync::{Arc, Weak},
|
sync::{Arc, Weak},
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
use telemetry::Telemetry;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use util::{ResultExt, TryFutureExt};
|
use util::{ResultExt, TryFutureExt};
|
||||||
|
@ -49,13 +56,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.report_event(
|
||||||
|
"test_telemetry",
|
||||||
|
json!({
|
||||||
|
"test_property": "test_value"
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,6 +86,7 @@ pub struct Client {
|
||||||
id: usize,
|
id: usize,
|
||||||
peer: Arc<Peer>,
|
peer: Arc<Peer>,
|
||||||
http: Arc<dyn HttpClient>,
|
http: Arc<dyn HttpClient>,
|
||||||
|
telemetry: Arc<Telemetry>,
|
||||||
state: RwLock<ClientState>,
|
state: RwLock<ClientState>,
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
|
@ -232,10 +256,11 @@ impl Drop for Subscription {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
pub fn new(http: Arc<dyn HttpClient>) -> Arc<Self> {
|
pub fn new(http: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
|
||||||
Arc::new(Self {
|
Arc::new(Self {
|
||||||
id: 0,
|
id: 0,
|
||||||
peer: Peer::new(),
|
peer: Peer::new(),
|
||||||
|
telemetry: Telemetry::new(http.clone(), cx),
|
||||||
http,
|
http,
|
||||||
state: Default::default(),
|
state: Default::default(),
|
||||||
|
|
||||||
|
@ -308,9 +333,11 @@ 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 => {
|
||||||
|
@ -339,6 +366,7 @@ impl Client {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
Status::SignedOut | Status::UpgradeRequired => {
|
Status::SignedOut | Status::UpgradeRequired => {
|
||||||
|
self.telemetry.set_user_id(user_id);
|
||||||
state._reconnect_task.take();
|
state._reconnect_task.take();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
@ -595,6 +623,9 @@ impl Client {
|
||||||
if credentials.is_none() && try_keychain {
|
if credentials.is_none() && try_keychain {
|
||||||
credentials = read_credentials_from_keychain(cx);
|
credentials = read_credentials_from_keychain(cx);
|
||||||
read_from_keychain = credentials.is_some();
|
read_from_keychain = credentials.is_some();
|
||||||
|
if read_from_keychain {
|
||||||
|
self.report_event("read credentials from keychain", Default::default());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if credentials.is_none() {
|
if credentials.is_none() {
|
||||||
let mut status_rx = self.status();
|
let mut status_rx = self.status();
|
||||||
|
@ -878,6 +909,7 @@ impl Client {
|
||||||
) -> Task<Result<Credentials>> {
|
) -> Task<Result<Credentials>> {
|
||||||
let platform = cx.platform();
|
let platform = cx.platform();
|
||||||
let executor = cx.background();
|
let executor = cx.background();
|
||||||
|
let telemetry = self.telemetry.clone();
|
||||||
executor.clone().spawn(async move {
|
executor.clone().spawn(async move {
|
||||||
// Generate a pair of asymmetric encryption keys. The public key will be used by the
|
// Generate a pair of asymmetric encryption keys. The public key will be used by the
|
||||||
// zed server to encrypt the user's access token, so that it can'be intercepted by
|
// zed server to encrypt the user's access token, so that it can'be intercepted by
|
||||||
|
@ -956,6 +988,8 @@ impl Client {
|
||||||
.context("failed to decrypt access token")?;
|
.context("failed to decrypt access token")?;
|
||||||
platform.activate(true);
|
platform.activate(true);
|
||||||
|
|
||||||
|
telemetry.report_event("authenticate with browser", Default::default());
|
||||||
|
|
||||||
Ok(Credentials {
|
Ok(Credentials {
|
||||||
user_id: user_id.parse()?,
|
user_id: user_id.parse()?,
|
||||||
access_token,
|
access_token,
|
||||||
|
@ -1020,6 +1054,18 @@ impl Client {
|
||||||
log::debug!("rpc respond. client_id:{}. name:{}", self.id, T::NAME);
|
log::debug!("rpc respond. client_id:{}. name:{}", self.id, T::NAME);
|
||||||
self.peer.respond_with_error(receipt, error)
|
self.peer.respond_with_error(receipt, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn start_telemetry(&self, db: Arc<Db>) {
|
||||||
|
self.telemetry.start(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn report_event(&self, kind: &str, properties: Value) {
|
||||||
|
self.telemetry.report_event(kind, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn telemetry_log_file_path(&self) -> Option<PathBuf> {
|
||||||
|
self.telemetry.log_file_path()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AnyWeakEntityHandle {
|
impl AnyWeakEntityHandle {
|
||||||
|
@ -1085,7 +1131,7 @@ mod tests {
|
||||||
cx.foreground().forbid_parking();
|
cx.foreground().forbid_parking();
|
||||||
|
|
||||||
let user_id = 5;
|
let user_id = 5;
|
||||||
let client = Client::new(FakeHttpClient::with_404_response());
|
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||||
let mut status = client.status();
|
let mut status = client.status();
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
|
@ -1124,7 +1170,7 @@ mod tests {
|
||||||
|
|
||||||
let auth_count = Arc::new(Mutex::new(0));
|
let auth_count = Arc::new(Mutex::new(0));
|
||||||
let dropped_auth_count = Arc::new(Mutex::new(0));
|
let dropped_auth_count = Arc::new(Mutex::new(0));
|
||||||
let client = Client::new(FakeHttpClient::with_404_response());
|
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
client.override_authenticate({
|
client.override_authenticate({
|
||||||
let auth_count = auth_count.clone();
|
let auth_count = auth_count.clone();
|
||||||
let dropped_auth_count = dropped_auth_count.clone();
|
let dropped_auth_count = dropped_auth_count.clone();
|
||||||
|
@ -1173,7 +1219,7 @@ mod tests {
|
||||||
cx.foreground().forbid_parking();
|
cx.foreground().forbid_parking();
|
||||||
|
|
||||||
let user_id = 5;
|
let user_id = 5;
|
||||||
let client = Client::new(FakeHttpClient::with_404_response());
|
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||||
|
|
||||||
let (done_tx1, mut done_rx1) = smol::channel::unbounded();
|
let (done_tx1, mut done_rx1) = smol::channel::unbounded();
|
||||||
|
@ -1219,7 +1265,7 @@ mod tests {
|
||||||
cx.foreground().forbid_parking();
|
cx.foreground().forbid_parking();
|
||||||
|
|
||||||
let user_id = 5;
|
let user_id = 5;
|
||||||
let client = Client::new(FakeHttpClient::with_404_response());
|
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||||
|
|
||||||
let model = cx.add_model(|_| Model::default());
|
let model = cx.add_model(|_| Model::default());
|
||||||
|
@ -1247,7 +1293,7 @@ mod tests {
|
||||||
cx.foreground().forbid_parking();
|
cx.foreground().forbid_parking();
|
||||||
|
|
||||||
let user_id = 5;
|
let user_id = 5;
|
||||||
let client = Client::new(FakeHttpClient::with_404_response());
|
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||||
|
|
||||||
let model = cx.add_model(|_| Model::default());
|
let model = cx.add_model(|_| Model::default());
|
||||||
|
|
255
crates/client/src/telemetry.rs
Normal file
255
crates/client/src/telemetry.rs
Normal file
|
@ -0,0 +1,255 @@
|
||||||
|
use crate::http::HttpClient;
|
||||||
|
use db::Db;
|
||||||
|
use gpui::{
|
||||||
|
executor::Background,
|
||||||
|
serde_json::{self, value::Map, Value},
|
||||||
|
AppContext, Task,
|
||||||
|
};
|
||||||
|
use isahc::Request;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::{
|
||||||
|
io::Write,
|
||||||
|
mem,
|
||||||
|
path::PathBuf,
|
||||||
|
sync::Arc,
|
||||||
|
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
use util::{post_inc, ResultExt, TryFutureExt};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub struct Telemetry {
|
||||||
|
http_client: Arc<dyn HttpClient>,
|
||||||
|
executor: Arc<Background>,
|
||||||
|
session_id: u128,
|
||||||
|
state: Mutex<TelemetryState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct TelemetryState {
|
||||||
|
user_id: Option<Arc<str>>,
|
||||||
|
device_id: Option<Arc<str>>,
|
||||||
|
app_version: Option<Arc<str>>,
|
||||||
|
os_version: Option<Arc<str>>,
|
||||||
|
os_name: &'static str,
|
||||||
|
queue: Vec<AmplitudeEvent>,
|
||||||
|
next_event_id: usize,
|
||||||
|
flush_task: Option<Task<()>>,
|
||||||
|
log_file: Option<NamedTempFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const AMPLITUDE_EVENTS_URL: &'static str = "https://api2.amplitude.com/batch";
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref AMPLITUDE_API_KEY: Option<String> = std::env::var("ZED_AMPLITUDE_API_KEY")
|
||||||
|
.ok()
|
||||||
|
.or_else(|| option_env!("ZED_AMPLITUDE_API_KEY").map(|key| key.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct AmplitudeEventBatch {
|
||||||
|
api_key: &'static str,
|
||||||
|
events: Vec<AmplitudeEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct AmplitudeEvent {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
user_id: Option<Arc<str>>,
|
||||||
|
device_id: Option<Arc<str>>,
|
||||||
|
event_type: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
event_properties: Option<Map<String, Value>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
user_properties: Option<Map<String, Value>>,
|
||||||
|
os_name: &'static str,
|
||||||
|
os_version: Option<Arc<str>>,
|
||||||
|
app_version: Option<Arc<str>>,
|
||||||
|
event_id: usize,
|
||||||
|
session_id: u128,
|
||||||
|
time: u128,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
const MAX_QUEUE_LEN: usize = 1;
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
const MAX_QUEUE_LEN: usize = 10;
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
|
impl Telemetry {
|
||||||
|
pub fn new(client: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
|
||||||
|
let platform = cx.platform();
|
||||||
|
let this = Arc::new(Self {
|
||||||
|
http_client: client,
|
||||||
|
executor: cx.background().clone(),
|
||||||
|
session_id: SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis(),
|
||||||
|
state: Mutex::new(TelemetryState {
|
||||||
|
os_version: platform
|
||||||
|
.os_version()
|
||||||
|
.log_err()
|
||||||
|
.map(|v| v.to_string().into()),
|
||||||
|
os_name: platform.os_name().into(),
|
||||||
|
app_version: platform
|
||||||
|
.app_version()
|
||||||
|
.log_err()
|
||||||
|
.map(|v| v.to_string().into()),
|
||||||
|
device_id: None,
|
||||||
|
queue: Default::default(),
|
||||||
|
flush_task: Default::default(),
|
||||||
|
next_event_id: 0,
|
||||||
|
log_file: None,
|
||||||
|
user_id: None,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if AMPLITUDE_API_KEY.is_some() {
|
||||||
|
this.executor
|
||||||
|
.spawn({
|
||||||
|
let this = this.clone();
|
||||||
|
async move {
|
||||||
|
if let Some(tempfile) = NamedTempFile::new().log_err() {
|
||||||
|
this.state.lock().log_file = Some(tempfile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_file_path(&self) -> Option<PathBuf> {
|
||||||
|
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(self: &Arc<Self>, db: Arc<Db>) {
|
||||||
|
let this = self.clone();
|
||||||
|
self.executor
|
||||||
|
.spawn(
|
||||||
|
async move {
|
||||||
|
let device_id = if let Some(device_id) = db
|
||||||
|
.read(["device_id"])?
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.next()
|
||||||
|
.and_then(|bytes| String::from_utf8(bytes).ok())
|
||||||
|
{
|
||||||
|
device_id
|
||||||
|
} else {
|
||||||
|
let device_id = Uuid::new_v4().to_string();
|
||||||
|
db.write([("device_id", device_id.as_bytes())])?;
|
||||||
|
device_id
|
||||||
|
};
|
||||||
|
|
||||||
|
let device_id = Some(Arc::from(device_id));
|
||||||
|
let mut state = this.state.lock();
|
||||||
|
state.device_id = device_id.clone();
|
||||||
|
for event in &mut state.queue {
|
||||||
|
event.device_id = device_id.clone();
|
||||||
|
}
|
||||||
|
if !state.queue.is_empty() {
|
||||||
|
drop(state);
|
||||||
|
this.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
.log_err(),
|
||||||
|
)
|
||||||
|
.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 report_event(self: &Arc<Self>, kind: &str, properties: Value) {
|
||||||
|
if AMPLITUDE_API_KEY.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut state = self.state.lock();
|
||||||
|
let event = AmplitudeEvent {
|
||||||
|
event_type: kind.to_string(),
|
||||||
|
time: SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis(),
|
||||||
|
session_id: self.session_id,
|
||||||
|
event_properties: if let Value::Object(properties) = properties {
|
||||||
|
Some(properties)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
user_properties: None,
|
||||||
|
user_id: state.user_id.clone(),
|
||||||
|
device_id: state.device_id.clone(),
|
||||||
|
os_name: state.os_name,
|
||||||
|
os_version: state.os_version.clone(),
|
||||||
|
app_version: state.app_version.clone(),
|
||||||
|
event_id: post_inc(&mut state.next_event_id),
|
||||||
|
};
|
||||||
|
state.queue.push(event);
|
||||||
|
if state.device_id.is_some() {
|
||||||
|
if state.queue.len() >= MAX_QUEUE_LEN {
|
||||||
|
drop(state);
|
||||||
|
self.flush();
|
||||||
|
} else {
|
||||||
|
let this = self.clone();
|
||||||
|
let executor = self.executor.clone();
|
||||||
|
state.flush_task = Some(self.executor.spawn(async move {
|
||||||
|
executor.timer(DEBOUNCE_INTERVAL).await;
|
||||||
|
this.flush();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(self: &Arc<Self>) {
|
||||||
|
let mut state = self.state.lock();
|
||||||
|
let events = mem::take(&mut state.queue);
|
||||||
|
state.flush_task.take();
|
||||||
|
drop(state);
|
||||||
|
|
||||||
|
if let Some(api_key) = AMPLITUDE_API_KEY.as_ref() {
|
||||||
|
let this = self.clone();
|
||||||
|
self.executor
|
||||||
|
.spawn(
|
||||||
|
async move {
|
||||||
|
let mut json_bytes = Vec::new();
|
||||||
|
|
||||||
|
if let Some(file) = &mut this.state.lock().log_file {
|
||||||
|
let file = file.as_file_mut();
|
||||||
|
for event in &events {
|
||||||
|
json_bytes.clear();
|
||||||
|
serde_json::to_writer(&mut json_bytes, event)?;
|
||||||
|
file.write_all(&json_bytes)?;
|
||||||
|
file.write(b"\n")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let batch = AmplitudeEventBatch { api_key, events };
|
||||||
|
json_bytes.clear();
|
||||||
|
serde_json::to_writer(&mut json_bytes, &batch)?;
|
||||||
|
let request =
|
||||||
|
Request::post(AMPLITUDE_EVENTS_URL).body(json_bytes.into())?;
|
||||||
|
this.http_client.send(request).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
.log_err(),
|
||||||
|
)
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
DROP TABLE signups;
|
||||||
|
|
||||||
|
ALTER TABLE users
|
||||||
|
DROP COLUMN github_user_id;
|
||||||
|
|
||||||
|
DROP INDEX index_users_on_email_address;
|
|
@ -0,0 +1,27 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS "signups" (
|
||||||
|
"id" SERIAL PRIMARY KEY,
|
||||||
|
"email_address" VARCHAR NOT NULL,
|
||||||
|
"email_confirmation_code" VARCHAR(64) NOT NULL,
|
||||||
|
"email_confirmation_sent" BOOLEAN NOT NULL,
|
||||||
|
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"device_id" VARCHAR,
|
||||||
|
"user_id" INTEGER REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
"inviting_user_id" INTEGER REFERENCES users (id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
"platform_mac" BOOLEAN NOT NULL,
|
||||||
|
"platform_linux" BOOLEAN NOT NULL,
|
||||||
|
"platform_windows" BOOLEAN NOT NULL,
|
||||||
|
"platform_unknown" BOOLEAN NOT NULL,
|
||||||
|
|
||||||
|
"editor_features" VARCHAR[],
|
||||||
|
"programming_languages" VARCHAR[]
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "index_signups_on_email_address" ON "signups" ("email_address");
|
||||||
|
CREATE INDEX "index_signups_on_email_confirmation_sent" ON "signups" ("email_confirmation_sent");
|
||||||
|
|
||||||
|
ALTER TABLE "users"
|
||||||
|
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");
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
auth,
|
auth,
|
||||||
db::{ProjectId, User, UserId},
|
db::{Invite, NewUserParams, ProjectId, Signup, User, UserId, WaitlistSummary},
|
||||||
rpc::{self, ResultExt},
|
rpc::{self, ResultExt},
|
||||||
AppState, Error, Result,
|
AppState, Error, Result,
|
||||||
};
|
};
|
||||||
|
@ -25,12 +25,8 @@ 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("/users", get(get_users).post(create_user))
|
.route("/users", get(get_users).post(create_user))
|
||||||
.route(
|
.route("/users/:id", put(update_user).delete(destroy_user))
|
||||||
"/users/:id",
|
|
||||||
put(update_user).delete(destroy_user).get(get_user),
|
|
||||||
)
|
|
||||||
.route("/users/:id/access_tokens", post(create_access_token))
|
.route("/users/:id/access_tokens", post(create_access_token))
|
||||||
.route("/bulk_users", post(create_users))
|
|
||||||
.route("/users_with_no_invites", get(get_users_with_no_invites))
|
.route("/users_with_no_invites", get(get_users_with_no_invites))
|
||||||
.route("/invite_codes/:code", get(get_user_for_invite_code))
|
.route("/invite_codes/:code", get(get_user_for_invite_code))
|
||||||
.route("/panic", post(trace_panic))
|
.route("/panic", post(trace_panic))
|
||||||
|
@ -45,6 +41,11 @@ pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Bod
|
||||||
)
|
)
|
||||||
.route("/user_activity/counts", get(get_active_user_counts))
|
.route("/user_activity/counts", get(get_active_user_counts))
|
||||||
.route("/project_metadata", get(get_project_metadata))
|
.route("/project_metadata", get(get_project_metadata))
|
||||||
|
.route("/signups", post(create_signup))
|
||||||
|
.route("/signups_summary", get(get_waitlist_summary))
|
||||||
|
.route("/user_invites", post(create_invite_from_code))
|
||||||
|
.route("/unsent_invites", get(get_unsent_invites))
|
||||||
|
.route("/sent_invites", post(record_sent_invites))
|
||||||
.layer(
|
.layer(
|
||||||
ServiceBuilder::new()
|
ServiceBuilder::new()
|
||||||
.layer(Extension(state))
|
.layer(Extension(state))
|
||||||
|
@ -86,6 +87,8 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
|
||||||
|
|
||||||
#[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>,
|
||||||
|
@ -95,6 +98,14 @@ 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) = ¶ms.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?
|
||||||
|
@ -108,40 +119,61 @@ async fn get_users(
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct CreateUserParams {
|
struct CreateUserParams {
|
||||||
|
github_user_id: i32,
|
||||||
github_login: String,
|
github_login: String,
|
||||||
invite_code: Option<String>,
|
email_address: String,
|
||||||
email_address: Option<String>,
|
email_confirmation_code: Option<String>,
|
||||||
admin: bool,
|
#[serde(default)]
|
||||||
|
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_id = if let Some(invite_code) = params.invite_code {
|
let user = NewUserParams {
|
||||||
let invitee_id = app
|
github_login: params.github_login,
|
||||||
|
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
|
.db
|
||||||
.redeem_invite_code(
|
.create_user_from_invite(
|
||||||
&invite_code,
|
&Invite {
|
||||||
¶ms.github_login,
|
email_address: params.email_address,
|
||||||
params.email_address.as_deref(),
|
email_confirmation_code,
|
||||||
|
},
|
||||||
|
user,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
rpc_server
|
user_id = result.user_id;
|
||||||
.invite_code_redeemed(&invite_code, invitee_id)
|
signup_device_id = result.signup_device_id;
|
||||||
.await
|
if let Some(inviter_id) = result.inviting_user_id {
|
||||||
.trace_err();
|
rpc_server
|
||||||
invitee_id
|
.invite_code_redeemed(inviter_id, user_id)
|
||||||
} else {
|
.await
|
||||||
app.db
|
.trace_err();
|
||||||
.create_user(
|
}
|
||||||
¶ms.github_login,
|
}
|
||||||
params.email_address.as_deref(),
|
// Creating a user as an admin
|
||||||
params.admin,
|
else {
|
||||||
)
|
user_id = app
|
||||||
.await?
|
.db
|
||||||
};
|
.create_user(¶ms.email_address, false, user)
|
||||||
|
.await?;
|
||||||
|
signup_device_id = None;
|
||||||
|
}
|
||||||
|
|
||||||
let user = app
|
let user = app
|
||||||
.db
|
.db
|
||||||
|
@ -149,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)]
|
||||||
|
@ -171,7 +206,9 @@ async fn update_user(
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(invite_count) = params.invite_count {
|
if let Some(invite_count) = params.invite_count {
|
||||||
app.db.set_invite_count(user_id, invite_count).await?;
|
app.db
|
||||||
|
.set_invite_count_for_user(user_id, invite_count)
|
||||||
|
.await?;
|
||||||
rpc_server.invite_count_updated(user_id).await.trace_err();
|
rpc_server.invite_count_updated(user_id).await.trace_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,54 +223,6 @@ async fn destroy_user(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_user(
|
|
||||||
Path(login): Path<String>,
|
|
||||||
Extension(app): Extension<Arc<AppState>>,
|
|
||||||
) -> Result<Json<User>> {
|
|
||||||
let user = app
|
|
||||||
.db
|
|
||||||
.get_user_by_github_login(&login)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "User not found".to_string()))?;
|
|
||||||
Ok(Json(user))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct CreateUsersParams {
|
|
||||||
users: Vec<CreateUsersEntry>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct CreateUsersEntry {
|
|
||||||
github_login: String,
|
|
||||||
email_address: String,
|
|
||||||
invite_count: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_users(
|
|
||||||
Json(params): Json<CreateUsersParams>,
|
|
||||||
Extension(app): Extension<Arc<AppState>>,
|
|
||||||
) -> Result<Json<Vec<User>>> {
|
|
||||||
let user_ids = app
|
|
||||||
.db
|
|
||||||
.create_users(
|
|
||||||
params
|
|
||||||
.users
|
|
||||||
.into_iter()
|
|
||||||
.map(|params| {
|
|
||||||
(
|
|
||||||
params.github_login,
|
|
||||||
params.email_address,
|
|
||||||
params.invite_count,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let users = app.db.get_users_by_ids(user_ids).await?;
|
|
||||||
Ok(Json(users))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct GetUsersWithNoInvites {
|
struct GetUsersWithNoInvites {
|
||||||
invited_by_another_user: bool,
|
invited_by_another_user: bool,
|
||||||
|
@ -368,22 +357,24 @@ struct CreateAccessTokenResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_access_token(
|
async fn create_access_token(
|
||||||
Path(login): Path<String>,
|
Path(user_id): Path<UserId>,
|
||||||
Query(params): Query<CreateAccessTokenQueryParams>,
|
Query(params): Query<CreateAccessTokenQueryParams>,
|
||||||
Extension(app): Extension<Arc<AppState>>,
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
) -> Result<Json<CreateAccessTokenResponse>> {
|
) -> Result<Json<CreateAccessTokenResponse>> {
|
||||||
// request.require_token().await?;
|
|
||||||
|
|
||||||
let user = app
|
let user = app
|
||||||
.db
|
.db
|
||||||
.get_user_by_github_login(&login)
|
.get_user_by_id(user_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| anyhow!("user not found"))?;
|
.ok_or_else(|| anyhow!("user not found"))?;
|
||||||
|
|
||||||
let mut user_id = user.id;
|
let mut user_id = user.id;
|
||||||
if let Some(impersonate) = params.impersonate {
|
if let Some(impersonate) = params.impersonate {
|
||||||
if user.admin {
|
if user.admin {
|
||||||
if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? {
|
if let Some(impersonated_user) = app
|
||||||
|
.db
|
||||||
|
.get_user_by_github_account(&impersonate, None)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
user_id = impersonated_user.id;
|
user_id = impersonated_user.id;
|
||||||
} else {
|
} else {
|
||||||
return Err(Error::Http(
|
return Err(Error::Http(
|
||||||
|
@ -415,3 +406,59 @@ async fn get_user_for_invite_code(
|
||||||
) -> Result<Json<User>> {
|
) -> Result<Json<User>> {
|
||||||
Ok(Json(app.db.get_user_for_invite_code(&code).await?))
|
Ok(Json(app.db.get_user_for_invite_code(&code).await?))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn create_signup(
|
||||||
|
Json(params): Json<Signup>,
|
||||||
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
app.db.create_signup(params).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_waitlist_summary(
|
||||||
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
|
) -> Result<Json<WaitlistSummary>> {
|
||||||
|
Ok(Json(app.db.get_waitlist_summary().await?))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CreateInviteFromCodeParams {
|
||||||
|
invite_code: String,
|
||||||
|
email_address: String,
|
||||||
|
device_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_invite_from_code(
|
||||||
|
Json(params): Json<CreateInviteFromCodeParams>,
|
||||||
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
|
) -> Result<Json<Invite>> {
|
||||||
|
Ok(Json(
|
||||||
|
app.db
|
||||||
|
.create_invite_from_code(
|
||||||
|
¶ms.invite_code,
|
||||||
|
¶ms.email_address,
|
||||||
|
params.device_id.as_deref(),
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct GetUnsentInvitesParams {
|
||||||
|
pub count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_unsent_invites(
|
||||||
|
Query(params): Query<GetUnsentInvitesParams>,
|
||||||
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
|
) -> Result<Json<Vec<Invite>>> {
|
||||||
|
Ok(Json(app.db.get_unsent_invites(params.count).await?))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn record_sent_invites(
|
||||||
|
Json(params): Json<Vec<Invite>>,
|
||||||
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
app.db.record_sent_invites(¶ms).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ mod db;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct GitHubUser {
|
struct GitHubUser {
|
||||||
id: usize,
|
id: i32,
|
||||||
login: String,
|
login: String,
|
||||||
email: Option<String>,
|
email: Option<String>,
|
||||||
}
|
}
|
||||||
|
@ -26,8 +26,11 @@ async fn main() {
|
||||||
let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var");
|
let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var");
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
let current_user =
|
let mut current_user =
|
||||||
fetch_github::<GitHubUser>(&client, &github_token, "https://api.github.com/user").await;
|
fetch_github::<GitHubUser>(&client, &github_token, "https://api.github.com/user").await;
|
||||||
|
current_user
|
||||||
|
.email
|
||||||
|
.get_or_insert_with(|| "placeholder@example.com".to_string());
|
||||||
let staff_users = fetch_github::<Vec<GitHubUser>>(
|
let staff_users = fetch_github::<Vec<GitHubUser>>(
|
||||||
&client,
|
&client,
|
||||||
&github_token,
|
&github_token,
|
||||||
|
@ -64,16 +67,24 @@ async fn main() {
|
||||||
let mut zed_user_ids = Vec::<UserId>::new();
|
let mut zed_user_ids = Vec::<UserId>::new();
|
||||||
for (github_user, admin) in zed_users {
|
for (github_user, admin) in zed_users {
|
||||||
if let Some(user) = db
|
if let Some(user) = db
|
||||||
.get_user_by_github_login(&github_user.login)
|
.get_user_by_github_account(&github_user.login, Some(github_user.id))
|
||||||
.await
|
.await
|
||||||
.expect("failed to fetch user")
|
.expect("failed to fetch user")
|
||||||
{
|
{
|
||||||
zed_user_ids.push(user.id);
|
zed_user_ids.push(user.id);
|
||||||
} else {
|
} else if let Some(email) = &github_user.email {
|
||||||
zed_user_ids.push(
|
zed_user_ids.push(
|
||||||
db.create_user(&github_user.login, github_user.email.as_deref(), admin)
|
db.create_user(
|
||||||
.await
|
email,
|
||||||
.expect("failed to insert user"),
|
admin,
|
||||||
|
db::NewUserParams {
|
||||||
|
github_login: github_user.login,
|
||||||
|
github_user_id: github_user.id,
|
||||||
|
invite_count: 5,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("failed to insert user"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
1289
crates/collab/src/db_tests.rs
Normal file
1289
crates/collab/src/db_tests.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{tests::TestDb, ProjectId, UserId},
|
db::{NewUserParams, ProjectId, TestDb, UserId},
|
||||||
rpc::{Executor, Server, Store},
|
rpc::{Executor, Server, Store},
|
||||||
AppState,
|
AppState,
|
||||||
};
|
};
|
||||||
|
@ -4652,7 +4652,18 @@ async fn test_random_collaboration(
|
||||||
|
|
||||||
let mut server = TestServer::start(cx.foreground(), cx.background()).await;
|
let mut server = TestServer::start(cx.foreground(), cx.background()).await;
|
||||||
let db = server.app_state.db.clone();
|
let db = server.app_state.db.clone();
|
||||||
let host_user_id = db.create_user("host", None, false).await.unwrap();
|
let host_user_id = db
|
||||||
|
.create_user(
|
||||||
|
"host@example.com",
|
||||||
|
false,
|
||||||
|
NewUserParams {
|
||||||
|
github_login: "host".into(),
|
||||||
|
github_user_id: 0,
|
||||||
|
invite_count: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
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(),
|
||||||
|
@ -4660,8 +4671,19 @@ async fn test_random_collaboration(
|
||||||
"guest-4".to_string(),
|
"guest-4".to_string(),
|
||||||
];
|
];
|
||||||
|
|
||||||
for username in &available_guests {
|
for (ix, username) in available_guests.iter().enumerate() {
|
||||||
let guest_user_id = db.create_user(username, None, false).await.unwrap();
|
let guest_user_id = db
|
||||||
|
.create_user(
|
||||||
|
&format!("{username}@example.com"),
|
||||||
|
false,
|
||||||
|
NewUserParams {
|
||||||
|
github_login: username.into(),
|
||||||
|
github_user_id: ix as i32,
|
||||||
|
invite_count: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
assert_eq!(*username, format!("guest-{}", guest_user_id));
|
assert_eq!(*username, format!("guest-{}", guest_user_id));
|
||||||
server
|
server
|
||||||
.app_state
|
.app_state
|
||||||
|
@ -5163,18 +5185,30 @@ impl TestServer {
|
||||||
});
|
});
|
||||||
|
|
||||||
let http = FakeHttpClient::with_404_response();
|
let http = FakeHttpClient::with_404_response();
|
||||||
let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
|
let user_id = if let Ok(Some(user)) = self
|
||||||
|
.app_state
|
||||||
|
.db
|
||||||
|
.get_user_by_github_account(name, None)
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
user.id
|
user.id
|
||||||
} else {
|
} else {
|
||||||
self.app_state
|
self.app_state
|
||||||
.db
|
.db
|
||||||
.create_user(name, None, false)
|
.create_user(
|
||||||
|
&format!("{name}@example.com"),
|
||||||
|
false,
|
||||||
|
NewUserParams {
|
||||||
|
github_login: name.into(),
|
||||||
|
github_user_id: 0,
|
||||||
|
invite_count: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
};
|
};
|
||||||
let client_name = name.to_string();
|
let client_name = name.to_string();
|
||||||
let mut client = Client::new(http.clone());
|
let mut client = cx.read(|cx| Client::new(http.clone(), cx));
|
||||||
let server = self.server.clone();
|
let server = self.server.clone();
|
||||||
let db = self.app_state.db.clone();
|
let db = self.app_state.db.clone();
|
||||||
let connection_killers = self.connection_killers.clone();
|
let connection_killers = self.connection_killers.clone();
|
||||||
|
|
|
@ -4,6 +4,8 @@ mod db;
|
||||||
mod env;
|
mod env;
|
||||||
mod rpc;
|
mod rpc;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod db_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod integration_tests;
|
mod integration_tests;
|
||||||
|
|
||||||
|
|
|
@ -541,27 +541,30 @@ impl Server {
|
||||||
|
|
||||||
pub async fn invite_code_redeemed(
|
pub async fn invite_code_redeemed(
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
code: &str,
|
inviter_id: UserId,
|
||||||
invitee_id: UserId,
|
invitee_id: UserId,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let user = self.app_state.db.get_user_for_invite_code(code).await?;
|
if let Some(user) = self.app_state.db.get_user_by_id(inviter_id).await? {
|
||||||
let store = self.store().await;
|
if let Some(code) = &user.invite_code {
|
||||||
let invitee_contact = store.contact_for_user(invitee_id, true);
|
let store = self.store().await;
|
||||||
for connection_id in store.connection_ids_for_user(user.id) {
|
let invitee_contact = store.contact_for_user(invitee_id, true);
|
||||||
self.peer.send(
|
for connection_id in store.connection_ids_for_user(inviter_id) {
|
||||||
connection_id,
|
self.peer.send(
|
||||||
proto::UpdateContacts {
|
connection_id,
|
||||||
contacts: vec![invitee_contact.clone()],
|
proto::UpdateContacts {
|
||||||
..Default::default()
|
contacts: vec![invitee_contact.clone()],
|
||||||
},
|
..Default::default()
|
||||||
)?;
|
},
|
||||||
self.peer.send(
|
)?;
|
||||||
connection_id,
|
self.peer.send(
|
||||||
proto::UpdateInviteInfo {
|
connection_id,
|
||||||
url: format!("{}{}", self.app_state.invite_link_prefix, code),
|
proto::UpdateInviteInfo {
|
||||||
count: user.invite_count as u32,
|
url: format!("{}{}", self.app_state.invite_link_prefix, &code),
|
||||||
},
|
count: user.invite_count as u32,
|
||||||
)?;
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1401,7 +1404,7 @@ impl Server {
|
||||||
let users = match query.len() {
|
let users = match query.len() {
|
||||||
0 => vec![],
|
0 => vec![],
|
||||||
1 | 2 => db
|
1 | 2 => db
|
||||||
.get_user_by_github_login(&query)
|
.get_user_by_github_account(&query, None)
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect(),
|
.collect(),
|
||||||
|
|
|
@ -1216,7 +1216,7 @@ mod tests {
|
||||||
|
|
||||||
let languages = Arc::new(LanguageRegistry::test());
|
let languages = Arc::new(LanguageRegistry::test());
|
||||||
let http_client = FakeHttpClient::with_404_response();
|
let http_client = FakeHttpClient::with_404_response();
|
||||||
let client = Client::new(http_client.clone());
|
let client = cx.read(|cx| Client::new(http_client.clone(), cx));
|
||||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
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 project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake()));
|
||||||
let server = FakeServer::for_client(current_user_id, &client, cx).await;
|
let server = FakeServer::for_client(current_user_id, &client, cx).await;
|
||||||
|
|
22
crates/db/Cargo.toml
Normal file
22
crates/db/Cargo.toml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
[package]
|
||||||
|
name = "db"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/db.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
collections = { path = "../collections" }
|
||||||
|
anyhow = "1.0.57"
|
||||||
|
async-trait = "0.1"
|
||||||
|
parking_lot = "0.11.1"
|
||||||
|
rocksdb = "0.18"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
|
tempdir = { version = "0.3.7" }
|
|
@ -30,6 +30,7 @@ use gpui::{
|
||||||
geometry::vector::{vec2f, Vector2F},
|
geometry::vector::{vec2f, Vector2F},
|
||||||
impl_actions, impl_internal_actions,
|
impl_actions, impl_internal_actions,
|
||||||
platform::CursorStyle,
|
platform::CursorStyle,
|
||||||
|
serde_json::json,
|
||||||
text_layout, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox,
|
text_layout, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox,
|
||||||
Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, Task, View,
|
Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, Task, View,
|
||||||
ViewContext, ViewHandle, WeakViewHandle,
|
ViewContext, ViewHandle, WeakViewHandle,
|
||||||
|
@ -1058,6 +1059,7 @@ impl Editor {
|
||||||
let editor_created_event = EditorCreated(cx.handle());
|
let editor_created_event = EditorCreated(cx.handle());
|
||||||
cx.emit_global(editor_created_event);
|
cx.emit_global(editor_created_event);
|
||||||
|
|
||||||
|
this.report_event("open editor", cx);
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5983,6 +5985,25 @@ impl Editor {
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn report_event(&self, name: &str, cx: &AppContext) {
|
||||||
|
if let Some((project, file)) = self.project.as_ref().zip(
|
||||||
|
self.buffer
|
||||||
|
.read(cx)
|
||||||
|
.as_singleton()
|
||||||
|
.and_then(|b| b.read(cx).file()),
|
||||||
|
) {
|
||||||
|
project.read(cx).client().report_event(
|
||||||
|
name,
|
||||||
|
json!({
|
||||||
|
"file_extension": file
|
||||||
|
.path()
|
||||||
|
.extension()
|
||||||
|
.and_then(|e| e.to_str())
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EditorSnapshot {
|
impl EditorSnapshot {
|
||||||
|
|
|
@ -404,6 +404,8 @@ impl Item for Editor {
|
||||||
project: ModelHandle<Project>,
|
project: ModelHandle<Project>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
|
self.report_event("save editor", cx);
|
||||||
|
|
||||||
let buffer = self.buffer().clone();
|
let buffer = self.buffer().clone();
|
||||||
let buffers = buffer.read(cx).all_buffers();
|
let buffers = buffer.read(cx).all_buffers();
|
||||||
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
|
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
|
||||||
|
|
|
@ -69,6 +69,8 @@ pub trait Platform: Send + Sync {
|
||||||
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
|
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
|
||||||
fn app_path(&self) -> Result<PathBuf>;
|
fn app_path(&self) -> Result<PathBuf>;
|
||||||
fn app_version(&self) -> Result<AppVersion>;
|
fn app_version(&self) -> Result<AppVersion>;
|
||||||
|
fn os_name(&self) -> &'static str;
|
||||||
|
fn os_version(&self) -> Result<AppVersion>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait ForegroundPlatform {
|
pub(crate) trait ForegroundPlatform {
|
||||||
|
|
|
@ -4,7 +4,7 @@ use super::{
|
||||||
use crate::{
|
use crate::{
|
||||||
executor, keymap,
|
executor, keymap,
|
||||||
platform::{self, CursorStyle},
|
platform::{self, CursorStyle},
|
||||||
Action, ClipboardItem, Event, Menu, MenuItem,
|
Action, AppVersion, ClipboardItem, Event, Menu, MenuItem,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use block::ConcreteBlock;
|
use block::ConcreteBlock;
|
||||||
|
@ -16,7 +16,8 @@ use cocoa::{
|
||||||
},
|
},
|
||||||
base::{id, nil, selector, YES},
|
base::{id, nil, selector, YES},
|
||||||
foundation::{
|
foundation::{
|
||||||
NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSString, NSUInteger, NSURL,
|
NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSString,
|
||||||
|
NSUInteger, NSURL,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use core_foundation::{
|
use core_foundation::{
|
||||||
|
@ -748,6 +749,22 @@ impl platform::Platform for MacPlatform {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn os_name(&self) -> &'static str {
|
||||||
|
"macOS"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn os_version(&self) -> Result<crate::AppVersion> {
|
||||||
|
unsafe {
|
||||||
|
let process_info = NSProcessInfo::processInfo(nil);
|
||||||
|
let version = process_info.operatingSystemVersion();
|
||||||
|
Ok(AppVersion {
|
||||||
|
major: version.majorVersion as usize,
|
||||||
|
minor: version.minorVersion as usize,
|
||||||
|
patch: version.patchVersion as usize,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn path_from_objc(path: id) -> PathBuf {
|
unsafe fn path_from_objc(path: id) -> PathBuf {
|
||||||
|
|
|
@ -196,6 +196,18 @@ impl super::Platform for Platform {
|
||||||
patch: 0,
|
patch: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn os_name(&self) -> &'static str {
|
||||||
|
"test"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn os_version(&self) -> Result<AppVersion> {
|
||||||
|
Ok(AppVersion {
|
||||||
|
major: 1,
|
||||||
|
minor: 0,
|
||||||
|
patch: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Window {
|
impl Window {
|
||||||
|
|
|
@ -10,6 +10,7 @@ doctest = false
|
||||||
[features]
|
[features]
|
||||||
test-support = [
|
test-support = [
|
||||||
"client/test-support",
|
"client/test-support",
|
||||||
|
"db/test-support",
|
||||||
"language/test-support",
|
"language/test-support",
|
||||||
"settings/test-support",
|
"settings/test-support",
|
||||||
"text/test-support",
|
"text/test-support",
|
||||||
|
@ -20,6 +21,7 @@ text = { path = "../text" }
|
||||||
client = { path = "../client" }
|
client = { path = "../client" }
|
||||||
clock = { path = "../clock" }
|
clock = { path = "../clock" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
|
db = { path = "../db" }
|
||||||
fsevent = { path = "../fsevent" }
|
fsevent = { path = "../fsevent" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
|
@ -54,6 +56,7 @@ rocksdb = "0.18"
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
client = { path = "../client", features = ["test-support"] }
|
client = { path = "../client", features = ["test-support"] }
|
||||||
collections = { path = "../collections", features = ["test-support"] }
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
|
db = { path = "../db", features = ["test-support"] }
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
language = { path = "../language", features = ["test-support"] }
|
language = { path = "../language", features = ["test-support"] }
|
||||||
lsp = { path = "../lsp", features = ["test-support"] }
|
lsp = { path = "../lsp", features = ["test-support"] }
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
mod db;
|
|
||||||
pub mod fs;
|
pub mod fs;
|
||||||
mod ignore;
|
mod ignore;
|
||||||
mod lsp_command;
|
mod lsp_command;
|
||||||
|
@ -666,7 +665,7 @@ impl Project {
|
||||||
|
|
||||||
let languages = Arc::new(LanguageRegistry::test());
|
let languages = Arc::new(LanguageRegistry::test());
|
||||||
let http_client = client::test::FakeHttpClient::with_404_response();
|
let http_client = client::test::FakeHttpClient::with_404_response();
|
||||||
let client = client::Client::new(http_client.clone());
|
let client = cx.update(|cx| client::Client::new(http_client.clone(), cx));
|
||||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
||||||
let project_store = cx.add_model(|_| ProjectStore::new(Db::open_fake()));
|
let project_store = cx.add_model(|_| ProjectStore::new(Db::open_fake()));
|
||||||
let project = cx.update(|cx| {
|
let project = cx.update(|cx| {
|
||||||
|
|
|
@ -2804,7 +2804,7 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let http_client = FakeHttpClient::with_404_response();
|
let http_client = FakeHttpClient::with_404_response();
|
||||||
let client = Client::new(http_client);
|
let client = cx.read(|cx| Client::new(http_client, cx));
|
||||||
|
|
||||||
let tree = Worktree::local(
|
let tree = Worktree::local(
|
||||||
client,
|
client,
|
||||||
|
@ -2866,8 +2866,7 @@ mod tests {
|
||||||
fs.insert_symlink("/root/lib/a/lib", "..".into()).await;
|
fs.insert_symlink("/root/lib/a/lib", "..".into()).await;
|
||||||
fs.insert_symlink("/root/lib/b/lib", "..".into()).await;
|
fs.insert_symlink("/root/lib/b/lib", "..".into()).await;
|
||||||
|
|
||||||
let http_client = FakeHttpClient::with_404_response();
|
let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
let client = Client::new(http_client);
|
|
||||||
let tree = Worktree::local(
|
let tree = Worktree::local(
|
||||||
client,
|
client,
|
||||||
Arc::from(Path::new("/root")),
|
Arc::from(Path::new("/root")),
|
||||||
|
@ -2945,8 +2944,7 @@ mod tests {
|
||||||
}));
|
}));
|
||||||
let dir = parent_dir.path().join("tree");
|
let dir = parent_dir.path().join("tree");
|
||||||
|
|
||||||
let http_client = FakeHttpClient::with_404_response();
|
let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
let client = Client::new(http_client.clone());
|
|
||||||
|
|
||||||
let tree = Worktree::local(
|
let tree = Worktree::local(
|
||||||
client,
|
client,
|
||||||
|
@ -3016,8 +3014,7 @@ mod tests {
|
||||||
"ignored-dir": {}
|
"ignored-dir": {}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let http_client = FakeHttpClient::with_404_response();
|
let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
let client = Client::new(http_client.clone());
|
|
||||||
|
|
||||||
let tree = Worktree::local(
|
let tree = Worktree::local(
|
||||||
client,
|
client,
|
||||||
|
@ -3064,8 +3061,7 @@ mod tests {
|
||||||
|
|
||||||
#[gpui::test(iterations = 30)]
|
#[gpui::test(iterations = 30)]
|
||||||
async fn test_create_directory(cx: &mut TestAppContext) {
|
async fn test_create_directory(cx: &mut TestAppContext) {
|
||||||
let http_client = FakeHttpClient::with_404_response();
|
let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
let client = Client::new(http_client.clone());
|
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
|
|
|
@ -856,7 +856,7 @@ impl AppState {
|
||||||
let fs = project::FakeFs::new(cx.background().clone());
|
let fs = project::FakeFs::new(cx.background().clone());
|
||||||
let languages = Arc::new(LanguageRegistry::test());
|
let languages = Arc::new(LanguageRegistry::test());
|
||||||
let http_client = client::test::FakeHttpClient::with_404_response();
|
let http_client = client::test::FakeHttpClient::with_404_response();
|
||||||
let client = Client::new(http_client.clone());
|
let client = Client::new(http_client.clone(), cx);
|
||||||
let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake()));
|
let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake()));
|
||||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
||||||
let themes = ThemeRegistry::new((), cx.font_cache().clone());
|
let themes = ThemeRegistry::new((), cx.font_cache().clone());
|
||||||
|
|
|
@ -3,6 +3,10 @@ use std::process::Command;
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.14");
|
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.14");
|
||||||
|
|
||||||
|
if let Ok(api_key) = std::env::var("ZED_AMPLITUDE_API_KEY") {
|
||||||
|
println!("cargo:rustc-env=ZED_AMPLITUDE_API_KEY={api_key}");
|
||||||
|
}
|
||||||
|
|
||||||
let output = Command::new("npm")
|
let output = Command::new("npm")
|
||||||
.current_dir("../../styles")
|
.current_dir("../../styles")
|
||||||
.args(["install", "--no-save"])
|
.args(["install", "--no-save"])
|
||||||
|
|
|
@ -20,7 +20,7 @@ use futures::{
|
||||||
FutureExt, SinkExt, StreamExt,
|
FutureExt, SinkExt, StreamExt,
|
||||||
};
|
};
|
||||||
use gpui::{executor::Background, App, AssetSource, AsyncAppContext, Task, ViewContext};
|
use gpui::{executor::Background, App, AssetSource, AsyncAppContext, Task, ViewContext};
|
||||||
use isahc::{config::Configurable, AsyncBody, Request};
|
use isahc::{config::Configurable, Request};
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
@ -88,7 +88,7 @@ fn main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
app.run(move |cx| {
|
app.run(move |cx| {
|
||||||
let client = client::Client::new(http.clone());
|
let client = client::Client::new(http.clone(), cx);
|
||||||
let mut languages = LanguageRegistry::new(login_shell_env_loaded);
|
let mut languages = LanguageRegistry::new(login_shell_env_loaded);
|
||||||
languages.set_language_server_download_dir(zed::paths::LANGUAGES_DIR.clone());
|
languages.set_language_server_download_dir(zed::paths::LANGUAGES_DIR.clone());
|
||||||
let languages = Arc::new(languages);
|
let languages = Arc::new(languages);
|
||||||
|
@ -121,7 +121,6 @@ fn main() {
|
||||||
vim::init(cx);
|
vim::init(cx);
|
||||||
terminal::init(cx);
|
terminal::init(cx);
|
||||||
|
|
||||||
let db = cx.background().block(db);
|
|
||||||
cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
|
cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
@ -139,6 +138,10 @@ fn main() {
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
let db = cx.background().block(db);
|
||||||
|
client.start_telemetry(db.clone());
|
||||||
|
client.report_event("start app", Default::default());
|
||||||
|
|
||||||
let project_store = cx.add_model(|_| ProjectStore::new(db.clone()));
|
let project_store = cx.add_model(|_| ProjectStore::new(db.clone()));
|
||||||
let app_state = Arc::new(AppState {
|
let app_state = Arc::new(AppState {
|
||||||
languages,
|
languages,
|
||||||
|
@ -280,12 +283,10 @@ fn init_panic_hook(app_version: String, http: Arc<dyn HttpClient>, background: A
|
||||||
"token": ZED_SECRET_CLIENT_TOKEN,
|
"token": ZED_SECRET_CLIENT_TOKEN,
|
||||||
}))
|
}))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let request = Request::builder()
|
let request = Request::post(&panic_report_url)
|
||||||
.uri(&panic_report_url)
|
|
||||||
.method(http::Method::POST)
|
|
||||||
.redirect_policy(isahc::config::RedirectPolicy::Follow)
|
.redirect_policy(isahc::config::RedirectPolicy::Follow)
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.body(AsyncBody::from(body))?;
|
.body(body.into())?;
|
||||||
let response = http.send(request).await.context("error sending panic")?;
|
let response = http.send(request).await.context("error sending panic")?;
|
||||||
if response.status().is_success() {
|
if response.status().is_success() {
|
||||||
fs::remove_file(child_path)
|
fs::remove_file(child_path)
|
||||||
|
|
|
@ -332,6 +332,11 @@ pub fn menus() -> Vec<Menu<'static>> {
|
||||||
action: Box::new(command_palette::Toggle),
|
action: Box::new(command_palette::Toggle),
|
||||||
},
|
},
|
||||||
MenuItem::Separator,
|
MenuItem::Separator,
|
||||||
|
MenuItem::Action {
|
||||||
|
name: "View Telemetry Log",
|
||||||
|
action: Box::new(crate::OpenTelemetryLog),
|
||||||
|
},
|
||||||
|
MenuItem::Separator,
|
||||||
MenuItem::Action {
|
MenuItem::Action {
|
||||||
name: "Documentation",
|
name: "Documentation",
|
||||||
action: Box::new(crate::OpenBrowser {
|
action: Box::new(crate::OpenBrowser {
|
||||||
|
|
|
@ -56,6 +56,7 @@ actions!(
|
||||||
DebugElements,
|
DebugElements,
|
||||||
OpenSettings,
|
OpenSettings,
|
||||||
OpenLog,
|
OpenLog,
|
||||||
|
OpenTelemetryLog,
|
||||||
OpenKeymap,
|
OpenKeymap,
|
||||||
OpenDefaultSettings,
|
OpenDefaultSettings,
|
||||||
OpenDefaultKeymap,
|
OpenDefaultKeymap,
|
||||||
|
@ -146,6 +147,12 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
||||||
open_log_file(workspace, app_state.clone(), cx);
|
open_log_file(workspace, app_state.clone(), cx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
cx.add_action({
|
||||||
|
let app_state = app_state.clone();
|
||||||
|
move |workspace: &mut Workspace, _: &OpenTelemetryLog, cx: &mut ViewContext<Workspace>| {
|
||||||
|
open_telemetry_log_file(workspace, app_state.clone(), cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
cx.add_action({
|
cx.add_action({
|
||||||
let app_state = app_state.clone();
|
let app_state = app_state.clone();
|
||||||
move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
|
move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
|
||||||
|
@ -504,6 +511,62 @@ fn open_log_file(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_telemetry_log_file(
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
app_state: Arc<AppState>,
|
||||||
|
cx: &mut ViewContext<Workspace>,
|
||||||
|
) {
|
||||||
|
workspace.with_local_workspace(cx, app_state.clone(), |_, cx| {
|
||||||
|
cx.spawn_weak(|workspace, mut cx| async move {
|
||||||
|
let workspace = workspace.upgrade(&cx)?;
|
||||||
|
let path = app_state.client.telemetry_log_file_path()?;
|
||||||
|
let log = app_state.fs.load(&path).await.log_err()?;
|
||||||
|
|
||||||
|
const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
|
||||||
|
let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
|
||||||
|
if let Some(newline_offset) = log[start_offset..].find('\n') {
|
||||||
|
start_offset += newline_offset + 1;
|
||||||
|
}
|
||||||
|
let log_suffix = &log[start_offset..];
|
||||||
|
|
||||||
|
workspace.update(&mut cx, |workspace, cx| {
|
||||||
|
let project = workspace.project().clone();
|
||||||
|
let buffer = project
|
||||||
|
.update(cx, |project, cx| project.create_buffer("", None, cx))
|
||||||
|
.expect("creating buffers on a local workspace always succeeds");
|
||||||
|
buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.set_language(app_state.languages.get_language("JSON"), cx);
|
||||||
|
buffer.edit(
|
||||||
|
[(
|
||||||
|
0..0,
|
||||||
|
concat!(
|
||||||
|
"// Zed collects anonymous usage data to help us understand how people are using the app.\n",
|
||||||
|
"// After the beta release, we'll provide the ability to opt out of this telemetry.\n",
|
||||||
|
"// Here is the data that has been reported for the current session:\n",
|
||||||
|
"\n"
|
||||||
|
),
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
buffer.edit([(buffer.len()..buffer.len(), log_suffix)], None, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
let buffer = cx.add_model(|cx| {
|
||||||
|
MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
|
||||||
|
});
|
||||||
|
workspace.add_item(
|
||||||
|
Box::new(cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(())
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn open_bundled_config_file(
|
fn open_bundled_config_file(
|
||||||
workspace: &mut Workspace,
|
workspace: &mut Workspace,
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue