From d7915840d06968a12988d47ff528b4acf0beb8a7 Mon Sep 17 00:00:00 2001 From: Joseph T Lyons Date: Wed, 19 Oct 2022 14:53:48 -0400 Subject: [PATCH 1/3] Switch to Mixpanel analytics Co-Authored-By: Max Brunsfeld --- .github/workflows/ci.yml | 2 +- .github/workflows/release_actions.yml | 11 -- .gitignore | 1 - crates/client/src/telemetry.rs | 188 ++++++++++++---------- crates/zed/build.rs | 4 +- script/amplitude_release/main.py | 30 ---- script/amplitude_release/requirements.txt | 1 - 7 files changed, 109 insertions(+), 128 deletions(-) delete mode 100644 script/amplitude_release/main.py delete mode 100644 script/amplitude_release/requirements.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 866d0acc0eb40355a09a0811e216cb8c5c0eeba0..a9516529321719dbecb215ed0e7c77237cef7a26 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }} APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }} - ZED_AMPLITUDE_API_KEY: ${{ secrets.ZED_AMPLITUDE_API_KEY }} + ZED_MIXPANEL_TOKEN: ${{ secrets.ZED_MIXPANEL_TOKEN }} steps: - name: Install Rust run: | diff --git a/.github/workflows/release_actions.yml b/.github/workflows/release_actions.yml index 9a3b2376df472eba470dc92fca7293d8a015206a..cf8293b963e22d0a94482e235458b20ccd738c89 100644 --- a/.github/workflows/release_actions.yml +++ b/.github/workflows/release_actions.yml @@ -20,14 +20,3 @@ jobs: ${{ github.event.release.body }} ``` - amplitude_release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: "3.10.5" - architecture: "x64" - cache: "pip" - - run: pip install -r script/amplitude_release/requirements.txt - - run: python script/amplitude_release/main.py ${{ github.event.release.tag_name }} ${{ secrets.ZED_AMPLITUDE_API_KEY }} ${{ secrets.ZED_AMPLITUDE_SECRET_KEY }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2d721f8ad2bcaeb943a590320ebb193653ce91e4..40a18ccc1d3d28a252f56edc42f92fca64c1df9d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,3 @@ /assets/themes/*.json /assets/themes/internal/*.json /assets/themes/experiments/*.json -**/venv \ No newline at end of file diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 6829eab53150901755c1f3d89025f3076acd34f0..3fafce09c0cba3e2b5dffa7dba7f06e18abcdf56 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -24,7 +24,6 @@ use uuid::Uuid; pub struct Telemetry { http_client: Arc, executor: Arc, - session_id: u128, state: Mutex, } @@ -35,43 +34,54 @@ struct TelemetryState { app_version: Option>, os_version: Option>, os_name: &'static str, - queue: Vec, + queue: Vec, next_event_id: usize, flush_task: Option>, log_file: Option, } -const AMPLITUDE_EVENTS_URL: &'static str = "https://api2.amplitude.com/batch"; +const MIXPANEL_EVENTS_URL: &'static str = "https://api.mixpanel.com/track"; +const MIXPANEL_ENGAGE_URL: &'static str = "https://api.mixpanel.com/engage#profile-set"; lazy_static! { - static ref AMPLITUDE_API_KEY: Option = std::env::var("ZED_AMPLITUDE_API_KEY") + static ref MIXPANEL_TOKEN: Option = std::env::var("ZED_MIXPANEL_TOKEN") .ok() - .or_else(|| option_env!("ZED_AMPLITUDE_API_KEY").map(|key| key.to_string())); + .or_else(|| option_env!("ZED_MIXPANEL_TOKEN").map(|key| key.to_string())); } -#[derive(Serialize)] -struct AmplitudeEventBatch { - api_key: &'static str, - events: Vec, +#[derive(Serialize, Debug)] +struct MixpanelEvent { + event: String, + properties: MixpanelEventProperties, } -#[derive(Serialize)] -struct AmplitudeEvent { - #[serde(skip_serializing_if = "Option::is_none")] - user_id: Option>, - device_id: Option>, - event_type: String, - #[serde(skip_serializing_if = "Option::is_none")] +#[derive(Serialize, Debug)] +struct MixpanelEventProperties { + // Mixpanel required fields + #[serde(skip_serializing_if = "str::is_empty")] + token: &'static str, + time: u128, + distinct_id: Option>, + #[serde(rename = "$insert_id")] + insert_id: usize, + // Custom fields + #[serde(skip_serializing_if = "Option::is_none", flatten)] event_properties: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - user_properties: Option>, os_name: &'static str, os_version: Option>, app_version: Option>, + signed_in: bool, platform: &'static str, - event_id: usize, - session_id: u128, - time: u128, +} + +#[derive(Serialize)] +struct MixpanelEngageRequest { + #[serde(rename = "$token")] + token: &'static str, + #[serde(rename = "$distinct_id")] + distinct_id: Arc, + #[serde(rename = "$set")] + set: Value, } #[cfg(debug_assertions)] @@ -92,10 +102,6 @@ impl Telemetry { 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() @@ -107,15 +113,15 @@ impl Telemetry { .log_err() .map(|v| v.to_string().into()), device_id: None, + metrics_id: None, queue: Default::default(), flush_task: Default::default(), next_event_id: 0, log_file: None, - metrics_id: None, }), }); - if AMPLITUDE_API_KEY.is_some() { + if MIXPANEL_TOKEN.is_some() { this.executor .spawn({ let this = this.clone(); @@ -148,11 +154,14 @@ impl Telemetry { device_id }; - let device_id = Some(Arc::from(device_id)); + let device_id: Arc = device_id.into(); let mut state = this.state.lock(); - state.device_id = device_id.clone(); + state.device_id = Some(device_id.clone()); for event in &mut state.queue { - event.device_id = device_id.clone(); + event + .properties + .distinct_id + .get_or_insert_with(|| device_id.clone()); } if !state.queue.is_empty() { drop(state); @@ -171,56 +180,63 @@ impl Telemetry { metrics_id: Option, is_staff: bool, ) { - let is_signed_in = metrics_id.is_some(); - self.state.lock().metrics_id = metrics_id.map(|s| s.into()); - if is_signed_in { - self.report_event_with_user_properties( - "$identify", - Default::default(), - json!({ "$set": { "staff": is_staff } }), - ) - } - } + let this = self.clone(); + let mut state = self.state.lock(); + let device_id = state.device_id.clone(); + let metrics_id: Option> = metrics_id.map(|id| id.into()); + state.metrics_id = metrics_id.clone(); + drop(state); - pub fn report_event(self: &Arc, kind: &str, properties: Value) { - self.report_event_with_user_properties(kind, properties, Default::default()); - } + if let Some((token, device_id)) = MIXPANEL_TOKEN.as_ref().zip(device_id) { + self.executor + .spawn( + async move { + let json_bytes = serde_json::to_vec(&[MixpanelEngageRequest { + token, + distinct_id: device_id, + set: json!({ "staff": is_staff, "id": metrics_id }), + }])?; - fn report_event_with_user_properties( - self: &Arc, - kind: &str, - properties: Value, - user_properties: Value, - ) { - if AMPLITUDE_API_KEY.is_none() { - return; + eprintln!("request: {}", std::str::from_utf8(&json_bytes).unwrap()); + + let request = Request::post(MIXPANEL_ENGAGE_URL) + .header("Content-Type", "application/json") + .body(json_bytes.into())?; + let response = this.http_client.send(request).await?; + + eprintln!("response: {:?} {:?}", response.status(), response.body()); + + Ok(()) + } + .log_err(), + ) + .detach(); } + } + pub fn report_event(self: &Arc, kind: &str, properties: Value) { 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: if let Value::Object(user_properties) = user_properties { - Some(user_properties) - } else { - None + let event = MixpanelEvent { + event: kind.to_string(), + properties: MixpanelEventProperties { + token: "", + time: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(), + distinct_id: state.device_id.clone(), + insert_id: post_inc(&mut state.next_event_id), + event_properties: if let Value::Object(properties) = properties { + Some(properties) + } else { + None + }, + os_name: state.os_name, + os_version: state.os_version.clone(), + app_version: state.app_version.clone(), + signed_in: state.metrics_id.is_some(), + platform: "Zed", }, - user_id: state.metrics_id.clone(), - device_id: state.device_id.clone(), - os_name: state.os_name, - platform: "Zed", - 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() { @@ -240,11 +256,11 @@ impl Telemetry { fn flush(self: &Arc) { let mut state = self.state.lock(); - let events = mem::take(&mut state.queue); + let mut events = mem::take(&mut state.queue); state.flush_task.take(); drop(state); - if let Some(api_key) = AMPLITUDE_API_KEY.as_ref() { + if let Some(token) = MIXPANEL_TOKEN.as_ref() { let this = self.clone(); self.executor .spawn( @@ -253,20 +269,28 @@ impl Telemetry { if let Some(file) = &mut this.state.lock().log_file { let file = file.as_file_mut(); - for event in &events { + for event in &mut events { json_bytes.clear(); serde_json::to_writer(&mut json_bytes, event)?; file.write_all(&json_bytes)?; file.write(b"\n")?; + + event.properties.token = token; } } - 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?; + serde_json::to_writer(&mut json_bytes, &events)?; + + eprintln!("request: {}", std::str::from_utf8(&json_bytes).unwrap()); + + let request = Request::post(MIXPANEL_EVENTS_URL) + .header("Content-Type", "application/json") + .body(json_bytes.into())?; + let response = this.http_client.send(request).await?; + + eprintln!("response: {:?} {:?}", response.status(), response.body()); + Ok(()) } .log_err(), diff --git a/crates/zed/build.rs b/crates/zed/build.rs index d3167851a00cf30656cb5160676c127af758d636..4a5643fd8e88a6228cdf477637c31a60a08f4b1a 100644 --- a/crates/zed/build.rs +++ b/crates/zed/build.rs @@ -3,8 +3,8 @@ use std::process::Command; fn main() { 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}"); + if let Ok(api_key) = std::env::var("ZED_MIXPANEL_TOKEN") { + println!("cargo:rustc-env=ZED_MIXPANEL_TOKEN={api_key}"); } let output = Command::new("npm") diff --git a/script/amplitude_release/main.py b/script/amplitude_release/main.py deleted file mode 100644 index 160e40b66c7c9e56380b9b18ac7cc6faaa9906ea..0000000000000000000000000000000000000000 --- a/script/amplitude_release/main.py +++ /dev/null @@ -1,30 +0,0 @@ -import datetime -import sys - -from amplitude_python_sdk.v2.clients.releases_client import ReleasesAPIClient -from amplitude_python_sdk.v2.models.releases import Release - - -def main(): - version = sys.argv[1] - version = version.removeprefix("v") - - api_key = sys.argv[2] - secret_key = sys.argv[3] - - current_datetime = datetime.datetime.now(datetime.timezone.utc) - current_datetime = current_datetime.strftime("%Y-%m-%d %H:%M:%S") - - release = Release( - title=version, - version=version, - release_start=current_datetime, - created_by="GitHub Release Workflow", - chart_visibility=True - ) - - ReleasesAPIClient(api_key=api_key, secret_key=secret_key).create(release) - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/script/amplitude_release/requirements.txt b/script/amplitude_release/requirements.txt deleted file mode 100644 index 7ed3ea65157f04eea2e2d9625eaf5147f7dddb9f..0000000000000000000000000000000000000000 --- a/script/amplitude_release/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -amplitude-python-sdk==0.2.0 \ No newline at end of file From 2f064d5ccc18edd6602b12527ad783838c5b529e Mon Sep 17 00:00:00 2001 From: Joseph T Lyons Date: Wed, 19 Oct 2022 17:30:00 -0400 Subject: [PATCH 2/3] Remove debug prints --- crates/client/src/telemetry.rs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 3fafce09c0cba3e2b5dffa7dba7f06e18abcdf56..02c1790664c28758cf366e43948b4aa2396032ec 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -196,16 +196,10 @@ impl Telemetry { distinct_id: device_id, set: json!({ "staff": is_staff, "id": metrics_id }), }])?; - - eprintln!("request: {}", std::str::from_utf8(&json_bytes).unwrap()); - let request = Request::post(MIXPANEL_ENGAGE_URL) .header("Content-Type", "application/json") .body(json_bytes.into())?; - let response = this.http_client.send(request).await?; - - eprintln!("response: {:?} {:?}", response.status(), response.body()); - + this.http_client.send(request).await?; Ok(()) } .log_err(), @@ -281,16 +275,10 @@ impl Telemetry { json_bytes.clear(); serde_json::to_writer(&mut json_bytes, &events)?; - - eprintln!("request: {}", std::str::from_utf8(&json_bytes).unwrap()); - let request = Request::post(MIXPANEL_EVENTS_URL) .header("Content-Type", "application/json") .body(json_bytes.into())?; - let response = this.http_client.send(request).await?; - - eprintln!("response: {:?} {:?}", response.status(), response.body()); - + this.http_client.send(request).await?; Ok(()) } .log_err(), From 022f70b1de54f4453281fc7f484f4e684b9ba5f6 Mon Sep 17 00:00:00 2001 From: Joseph T Lyons Date: Thu, 20 Oct 2022 19:27:55 -0400 Subject: [PATCH 3/3] Temporarily restore integration with Amplitude This will be reverted later, once we fully switch to Mixpanel --- .github/workflows/ci.yml | 1 + .github/workflows/release_actions.yml | 11 + .gitignore | 1 + crates/client/src/amplitude_telemetry.rs | 277 ++++++++++++++++++++++ crates/client/src/client.rs | 15 +- crates/client/src/user.rs | 11 + crates/zed/build.rs | 3 + script/amplitude_release/main.py | 30 +++ script/amplitude_release/requirements.txt | 1 + 9 files changed, 348 insertions(+), 2 deletions(-) create mode 100644 crates/client/src/amplitude_telemetry.rs create mode 100644 script/amplitude_release/main.py create mode 100644 script/amplitude_release/requirements.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9516529321719dbecb215ed0e7c77237cef7a26..3c9c9481e73aa6c02882b170213dfbce942ed6c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,7 @@ jobs: MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }} APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }} + ZED_AMPLITUDE_API_KEY: ${{ secrets.ZED_AMPLITUDE_API_KEY }} ZED_MIXPANEL_TOKEN: ${{ secrets.ZED_MIXPANEL_TOKEN }} steps: - name: Install Rust diff --git a/.github/workflows/release_actions.yml b/.github/workflows/release_actions.yml index cf8293b963e22d0a94482e235458b20ccd738c89..17cb8864e15f8c2994cb5c738d16458b89463f2c 100644 --- a/.github/workflows/release_actions.yml +++ b/.github/workflows/release_actions.yml @@ -20,3 +20,14 @@ jobs: ${{ github.event.release.body }} ``` + amplitude_release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.10.5" + architecture: "x64" + cache: "pip" + - run: pip install -r script/amplitude_release/requirements.txt + - run: python script/amplitude_release/main.py ${{ github.event.release.tag_name }} ${{ secrets.ZED_AMPLITUDE_API_KEY }} ${{ secrets.ZED_AMPLITUDE_SECRET_KEY }} diff --git a/.gitignore b/.gitignore index 40a18ccc1d3d28a252f56edc42f92fca64c1df9d..2d721f8ad2bcaeb943a590320ebb193653ce91e4 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ /assets/themes/*.json /assets/themes/internal/*.json /assets/themes/experiments/*.json +**/venv \ No newline at end of file diff --git a/crates/client/src/amplitude_telemetry.rs b/crates/client/src/amplitude_telemetry.rs new file mode 100644 index 0000000000000000000000000000000000000000..5db2bedf03016b59fc81c54fa5d1cf0f999e5140 --- /dev/null +++ b/crates/client/src/amplitude_telemetry.rs @@ -0,0 +1,277 @@ +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 serde_json::json; +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 AmplitudeTelemetry { + http_client: Arc, + executor: Arc, + session_id: u128, + state: Mutex, +} + +#[derive(Default)] +struct AmplitudeTelemetryState { + metrics_id: Option>, + device_id: Option>, + app_version: Option>, + os_version: Option>, + os_name: &'static str, + queue: Vec, + next_event_id: usize, + flush_task: Option>, + log_file: Option, +} + +const AMPLITUDE_EVENTS_URL: &'static str = "https://api2.amplitude.com/batch"; + +lazy_static! { + static ref AMPLITUDE_API_KEY: Option = 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, +} + +#[derive(Serialize)] +struct AmplitudeEvent { + #[serde(skip_serializing_if = "Option::is_none")] + user_id: Option>, + device_id: Option>, + event_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + event_properties: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + user_properties: Option>, + os_name: &'static str, + os_version: Option>, + app_version: Option>, + platform: &'static 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 AmplitudeTelemetry { + pub fn new(client: Arc, cx: &AppContext) -> Arc { + 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(AmplitudeTelemetryState { + 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, + metrics_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 { + Some(self.state.lock().log_file.as_ref()?.path().to_path_buf()) + } + + pub fn start(self: &Arc, db: Db) { + let this = self.clone(); + self.executor + .spawn( + async move { + let device_id = if let Ok(Some(device_id)) = db.read_kvp("device_id") { + device_id + } else { + let device_id = Uuid::new_v4().to_string(); + db.write_kvp("device_id", &device_id)?; + 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_authenticated_user_info( + self: &Arc, + metrics_id: Option, + is_staff: bool, + ) { + let is_signed_in = metrics_id.is_some(); + self.state.lock().metrics_id = metrics_id.map(|s| s.into()); + if is_signed_in { + self.report_event_with_user_properties( + "$identify", + Default::default(), + json!({ "$set": { "staff": is_staff } }), + ) + } + } + + pub fn report_event(self: &Arc, kind: &str, properties: Value) { + self.report_event_with_user_properties(kind, properties, Default::default()); + } + + fn report_event_with_user_properties( + self: &Arc, + kind: &str, + properties: Value, + user_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: if let Value::Object(user_properties) = user_properties { + Some(user_properties) + } else { + None + }, + user_id: state.metrics_id.clone(), + device_id: state.device_id.clone(), + os_name: state.os_name, + platform: "Zed", + 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) { + 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(); + } + } +} diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 3878cc90c3826ebbb64b924be2b847d53858f878..8f6d4aa10d798686d1b59e3c46c3129beeffd280 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1,11 +1,13 @@ #[cfg(any(test, feature = "test-support"))] pub mod test; +pub mod amplitude_telemetry; pub mod channel; pub mod http; pub mod telemetry; pub mod user; +use amplitude_telemetry::AmplitudeTelemetry; use anyhow::{anyhow, Context, Result}; use async_recursion::async_recursion; use async_tungstenite::tungstenite::{ @@ -82,6 +84,7 @@ pub struct Client { peer: Arc, http: Arc, telemetry: Arc, + amplitude_telemetry: Arc, state: RwLock, #[allow(clippy::type_complexity)] @@ -261,6 +264,7 @@ impl Client { id: 0, peer: Peer::new(), telemetry: Telemetry::new(http.clone(), cx), + amplitude_telemetry: AmplitudeTelemetry::new(http.clone(), cx), http, state: Default::default(), @@ -373,6 +377,8 @@ impl Client { } Status::SignedOut | Status::UpgradeRequired => { self.telemetry.set_authenticated_user_info(None, false); + self.amplitude_telemetry + .set_authenticated_user_info(None, false); state._reconnect_task.take(); } _ => {} @@ -1013,6 +1019,7 @@ impl Client { let platform = cx.platform(); let executor = cx.background(); let telemetry = self.telemetry.clone(); + let amplitude_telemetry = self.amplitude_telemetry.clone(); let http = self.http.clone(); executor.clone().spawn(async move { // Generate a pair of asymmetric encryption keys. The public key will be used by the @@ -1097,6 +1104,7 @@ impl Client { platform.activate(true); telemetry.report_event("authenticate with browser", Default::default()); + amplitude_telemetry.report_event("authenticate with browser", Default::default()); Ok(Credentials { user_id: user_id.parse()?, @@ -1208,14 +1216,17 @@ impl Client { } pub fn start_telemetry(&self, db: Db) { - self.telemetry.start(db); + self.telemetry.start(db.clone()); + self.amplitude_telemetry.start(db); } pub fn report_event(&self, kind: &str, properties: Value) { - self.telemetry.report_event(kind, properties) + self.telemetry.report_event(kind, properties.clone()); + self.amplitude_telemetry.report_event(kind, properties); } pub fn telemetry_log_file_path(&self) -> Option { + self.amplitude_telemetry.log_file_path(); self.telemetry.log_file_path() } } diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 3c3d7e7fb3e199e16e28c106deda13d473a9c840..d06a6682c5e0fb2589d19fa23909e0988794b9bc 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -143,13 +143,24 @@ impl UserStore { let (user, info) = futures::join!(fetch_user, fetch_metrics_id); if let Some(info) = info { client.telemetry.set_authenticated_user_info( + Some(info.metrics_id.clone()), + info.staff, + ); + client.amplitude_telemetry.set_authenticated_user_info( Some(info.metrics_id), info.staff, ); } else { client.telemetry.set_authenticated_user_info(None, false); + client + .amplitude_telemetry + .set_authenticated_user_info(None, false); } + client.telemetry.report_event("sign in", Default::default()); + client + .amplitude_telemetry + .report_event("sign in", Default::default()); current_user_tx.send(user).await.ok(); } } diff --git a/crates/zed/build.rs b/crates/zed/build.rs index 0de3a40dfb2bb5c3dd6b5c771866a94d4a3683a4..8f56bcd34069b97e72aeb3b5889970f10d3101c7 100644 --- a/crates/zed/build.rs +++ b/crates/zed/build.rs @@ -6,6 +6,9 @@ fn main() { if let Ok(api_key) = std::env::var("ZED_MIXPANEL_TOKEN") { println!("cargo:rustc-env=ZED_MIXPANEL_TOKEN={api_key}"); } + 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") .current_dir("../../styles") diff --git a/script/amplitude_release/main.py b/script/amplitude_release/main.py new file mode 100644 index 0000000000000000000000000000000000000000..160e40b66c7c9e56380b9b18ac7cc6faaa9906ea --- /dev/null +++ b/script/amplitude_release/main.py @@ -0,0 +1,30 @@ +import datetime +import sys + +from amplitude_python_sdk.v2.clients.releases_client import ReleasesAPIClient +from amplitude_python_sdk.v2.models.releases import Release + + +def main(): + version = sys.argv[1] + version = version.removeprefix("v") + + api_key = sys.argv[2] + secret_key = sys.argv[3] + + current_datetime = datetime.datetime.now(datetime.timezone.utc) + current_datetime = current_datetime.strftime("%Y-%m-%d %H:%M:%S") + + release = Release( + title=version, + version=version, + release_start=current_datetime, + created_by="GitHub Release Workflow", + chart_visibility=True + ) + + ReleasesAPIClient(api_key=api_key, secret_key=secret_key).create(release) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/script/amplitude_release/requirements.txt b/script/amplitude_release/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..7ed3ea65157f04eea2e2d9625eaf5147f7dddb9f --- /dev/null +++ b/script/amplitude_release/requirements.txt @@ -0,0 +1 @@ +amplitude-python-sdk==0.2.0 \ No newline at end of file