From 051763efb4cb897320c2d9e43e45361cdc9aeed6 Mon Sep 17 00:00:00 2001 From: Peter White Date: Wed, 1 May 2024 19:32:51 -0600 Subject: [PATCH 01/18] feat(telemetry): add telemetry for CLI metrics --- .gitignore | 6 +- Cargo.lock | 38 +++++- Cargo.toml | 20 ++-- crates/pop-cli/Cargo.toml | 4 + crates/pop-cli/src/commands/build/contract.rs | 6 + .../pop-cli/src/commands/build/parachain.rs | 6 + crates/pop-cli/src/commands/call/contract.rs | 5 + crates/pop-cli/src/commands/new/contract.rs | 7 ++ crates/pop-cli/src/commands/new/pallet.rs | 7 ++ crates/pop-cli/src/commands/new/parachain.rs | 6 + crates/pop-cli/src/commands/test/contract.rs | 12 ++ crates/pop-cli/src/commands/up/contract.rs | 6 + crates/pop-cli/src/commands/up/parachain.rs | 7 ++ crates/pop-cli/src/main.rs | 3 + crates/pop-telemetry/Cargo.toml | 13 +++ crates/pop-telemetry/src/lib.rs | 109 ++++++++++++++++++ 16 files changed, 244 insertions(+), 11 deletions(-) create mode 100644 crates/pop-telemetry/Cargo.toml create mode 100644 crates/pop-telemetry/src/lib.rs diff --git a/.gitignore b/.gitignore index 5fa23887..9a2038ec 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,8 @@ **/node_modules/ /src/x.rs -.DS_Store \ No newline at end of file +.DS_Store + +# IDEs +.idea +.vscode \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index b61ae280..0af67527 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3238,6 +3238,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "hostname" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" +dependencies = [ + "cfg-if", + "libc", + "windows", +] + [[package]] name = "hstr" version = "0.2.8" @@ -5475,7 +5486,9 @@ dependencies = [ "git2", "pop-contracts", "pop-parachains", + "pop-telemetry", "predicates", + "serde_json", "sp-core 30.0.0", "sp-weights", "strum 0.26.2", @@ -5531,6 +5544,19 @@ dependencies = [ "zombienet-support", ] +[[package]] +name = "pop-telemetry" +version = "0.1.0" +dependencies = [ + "hostname 0.4.0", + "log", + "reqwest", + "serde_json", + "thiserror", + "tokio", + "url", +] + [[package]] name = "portable-atomic" version = "1.6.0" @@ -5972,7 +5998,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" dependencies = [ - "hostname", + "hostname 0.3.1", "quick-error", ] @@ -9961,6 +9987,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.4", +] + [[package]] name = "windows-core" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 023946d4..f70bf318 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,28 +19,30 @@ duct = "0.13" git2 = "0.18" tempfile = "3.8" tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } -url = { version = "2.5"} +url = { version = "2.5" } +hostname = "0.4.0" +log = "0.4.20" # contracts -subxt-signer = { version = "0.34.0", features = ["subxt", "sr25519"]} +subxt-signer = { version = "0.34.0", features = ["subxt", "sr25519"] } subxt = { version = "0.34.0" } ink_env = { version = "5.0.0-rc.2" } -sp-core = { version = "30.0.0"} +sp-core = { version = "30.0.0" } sp-weights = { version = "29.0.0" } contract-build = { version = "4.0.2" } -contract-extrinsics = { version = "4.0.0-rc.3"} +contract-extrinsics = { version = "4.0.0-rc.3" } # parachains askama = "0.12" -regex="1.5.4" +regex = "1.5.4" walkdir = "2.4" -indexmap = { version = "2.2"} +indexmap = { version = "2.2" } toml_edit = { version = "0.22", features = ["serde"] } symlink = { version = "0.1" } -reqwest = { version = "0.11" } -serde_json = { version = "1.0"} +reqwest = { version = "0.11", features = ["json"] } +serde_json = { version = "1.0" } serde = { version = "1.0", features = ["derive"] } -zombienet-sdk = { git = "https://github.com/r0gue-io/zombienet-sdk", branch = "pop", version = "0.1.0-alpha.1"} +zombienet-sdk = { git = "https://github.com/r0gue-io/zombienet-sdk", branch = "pop", version = "0.1.0-alpha.1" } zombienet-support = { git = "https://github.com/r0gue-io/zombienet-sdk", branch = "pop", version = "0.1.0-alpha.1" } git2_credentials = "0.13.0" diff --git a/crates/pop-cli/Cargo.toml b/crates/pop-cli/Cargo.toml index 1d027f9e..81ba3356 100644 --- a/crates/pop-cli/Cargo.toml +++ b/crates/pop-cli/Cargo.toml @@ -14,6 +14,7 @@ duct.workspace = true tempfile.workspace = true url.workspace = true tokio.workspace = true +serde_json.workspace = true # pop-cli clap.workspace = true @@ -32,6 +33,9 @@ pop-parachains = { path = "../pop-parachains", optional = true } dirs = { version = "5.0", optional = true } git2.workspace = true +# telemetry +pop-telemetry = { path = "../pop-telemetry"} + [dev-dependencies] assert_cmd = "2.0.14" predicates = "3.1.0" diff --git a/crates/pop-cli/src/commands/build/contract.rs b/crates/pop-cli/src/commands/build/contract.rs index d222e62a..5df36643 100644 --- a/crates/pop-cli/src/commands/build/contract.rs +++ b/crates/pop-cli/src/commands/build/contract.rs @@ -19,6 +19,12 @@ impl BuildContractCommand { pub(crate) fn execute(&self) -> anyhow::Result<()> { clear_screen()?; intro(format!("{}: Building a contract", style(" Pop CLI ").black().on_magenta()))?; + + let _ = tokio::spawn(pop_telemetry::record_cli_command( + "build", + serde_json::json!({"contract": ""}), + )); + set_theme(Theme); let result_build = build_smart_contract(&self.path)?; diff --git a/crates/pop-cli/src/commands/build/parachain.rs b/crates/pop-cli/src/commands/build/parachain.rs index 0b8ad674..ea667e81 100644 --- a/crates/pop-cli/src/commands/build/parachain.rs +++ b/crates/pop-cli/src/commands/build/parachain.rs @@ -20,6 +20,12 @@ impl BuildParachainCommand { pub(crate) fn execute(&self) -> anyhow::Result<()> { clear_screen()?; intro(format!("{}: Building a parachain", style(" Pop CLI ").black().on_magenta()))?; + + let _ = tokio::spawn(pop_telemetry::record_cli_command( + "build", + serde_json::json!({"parachain": ""}), + )); + set_theme(Theme); build_parachain(&self.path)?; diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index f0e9ef82..6b45ea47 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -57,6 +57,11 @@ impl CallContractCommand { pub(crate) async fn execute(&self) -> anyhow::Result<()> { clear_screen()?; intro(format!("{}: Calling a contract", style(" Pop CLI ").black().on_magenta()))?; + let _ = tokio::spawn(pop_telemetry::record_cli_command( + "call", + serde_json::json!({"contract": ""}), + )); + set_theme(Theme); let call_exec = set_up_call(CallOpts { diff --git a/crates/pop-cli/src/commands/new/contract.rs b/crates/pop-cli/src/commands/new/contract.rs index b86a7530..572e2ccf 100644 --- a/crates/pop-cli/src/commands/new/contract.rs +++ b/crates/pop-cli/src/commands/new/contract.rs @@ -50,6 +50,13 @@ impl NewContractCommand { let mut spinner = cliclack::spinner(); spinner.start("Generating contract..."); create_smart_contract(&self.name, contract_path.as_path())?; + + let _ = tokio::spawn(pop_telemetry::record_cli_command( + "new", + serde_json::json!({"contract": "default"}), + )) + .await; + spinner.stop("Smart contract created!"); outro(format!("cd into \"{}\" and enjoy hacking! 🚀", contract_path.display()))?; Ok(()) diff --git a/crates/pop-cli/src/commands/new/pallet.rs b/crates/pop-cli/src/commands/new/pallet.rs index f729b5f9..2372f444 100644 --- a/crates/pop-cli/src/commands/new/pallet.rs +++ b/crates/pop-cli/src/commands/new/pallet.rs @@ -55,6 +55,13 @@ impl NewPalletCommand { description: self.description.clone().expect("default values"), }, )?; + + let _ = tokio::spawn(pop_telemetry::record_cli_command( + "new", + serde_json::json!({"pallet": "template"}), + )) + .await; + spinner.stop("Generation complete"); outro(format!("cd into \"{}\" and enjoy hacking! 🚀", &self.name))?; Ok(()) diff --git a/crates/pop-cli/src/commands/new/parachain.rs b/crates/pop-cli/src/commands/new/parachain.rs index 737b0808..efd2ec72 100644 --- a/crates/pop-cli/src/commands/new/parachain.rs +++ b/crates/pop-cli/src/commands/new/parachain.rs @@ -142,6 +142,12 @@ fn generate_parachain_from_template( template, provider ))?; + + tokio::spawn(pop_telemetry::record_cli_command( + "new", + serde_json::json!({"parachain": {provider.to_string(): template.to_string()}}), + )); + let destination_path = check_destination_path(name_template)?; let mut spinner = cliclack::spinner(); diff --git a/crates/pop-cli/src/commands/test/contract.rs b/crates/pop-cli/src/commands/test/contract.rs index 73a24b3f..edb6f1ef 100644 --- a/crates/pop-cli/src/commands/test/contract.rs +++ b/crates/pop-cli/src/commands/test/contract.rs @@ -19,15 +19,27 @@ pub(crate) struct TestContractCommand { impl TestContractCommand { pub(crate) fn execute(&self) -> anyhow::Result<()> { clear_screen()?; + if self.features.is_some() && self.features.clone().unwrap().contains("e2e-tests") { intro(format!( "{}: Starting end-to-end tests", style(" Pop CLI ").black().on_magenta() ))?; + + let _ = tokio::spawn(pop_telemetry::record_cli_command( + "test", + serde_json::json!({"contract": "e2e"}), + )); + test_e2e_smart_contract(&self.path)?; outro("End-to-end testing complete")?; } else { intro(format!("{}: Starting unit tests", style(" Pop CLI ").black().on_magenta()))?; + let _ = tokio::spawn(pop_telemetry::record_cli_command( + "test", + serde_json::json!({"contract": "unit"}), + )); + test_smart_contract(&self.path)?; outro("Unit testing complete")?; } diff --git a/crates/pop-cli/src/commands/up/contract.rs b/crates/pop-cli/src/commands/up/contract.rs index a6a76a01..aa9ed285 100644 --- a/crates/pop-cli/src/commands/up/contract.rs +++ b/crates/pop-cli/src/commands/up/contract.rs @@ -55,6 +55,12 @@ impl UpContractCommand { pub(crate) async fn execute(&self) -> anyhow::Result<()> { clear_screen()?; intro(format!("{}: Deploy a smart contract", style(" Pop CLI ").black().on_magenta()))?; + + let _ = tokio::spawn(pop_telemetry::record_cli_command( + "up", + serde_json::json!({"contract": ""}), + )); + let instantiate_exec = set_up_deployment(UpOpts { path: self.path.clone(), constructor: self.constructor.clone(), diff --git a/crates/pop-cli/src/commands/up/parachain.rs b/crates/pop-cli/src/commands/up/parachain.rs index 1c36c6e8..231ae23d 100644 --- a/crates/pop-cli/src/commands/up/parachain.rs +++ b/crates/pop-cli/src/commands/up/parachain.rs @@ -30,6 +30,13 @@ impl ZombienetCommand { pub(crate) async fn execute(&self) -> anyhow::Result<()> { clear_screen()?; intro(format!("{}: Deploy a parachain", style(" Pop CLI ").black().on_magenta()))?; + + let _ = tokio::spawn(pop_telemetry::record_cli_command( + "up", + serde_json::json!({"parachain": ""}), + )) + .await; + set_theme(Theme); // Parse arguments let cache = crate::cache()?; diff --git a/crates/pop-cli/src/main.rs b/crates/pop-cli/src/main.rs index af707305..01315d5a 100644 --- a/crates/pop-cli/src/main.rs +++ b/crates/pop-cli/src/main.rs @@ -40,6 +40,9 @@ enum Commands { #[tokio::main] async fn main() -> Result<()> { + // TODO: use tokio spawn + let _ = pop_telemetry::record_cli_used().await; + let cli = Cli::parse(); match cli.command { Commands::New(args) => Ok(match &args.command { diff --git a/crates/pop-telemetry/Cargo.toml b/crates/pop-telemetry/Cargo.toml new file mode 100644 index 00000000..f042ff82 --- /dev/null +++ b/crates/pop-telemetry/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pop-telemetry" +version = "0.1.0" +edition = "2021" + +[dependencies] +thiserror.workspace = true +tokio.workspace = true +url.workspace = true +reqwest.workspace = true +serde_json.workspace = true +hostname.workspace = true +log.workspace = true \ No newline at end of file diff --git a/crates/pop-telemetry/src/lib.rs b/crates/pop-telemetry/src/lib.rs new file mode 100644 index 00000000..c72f439d --- /dev/null +++ b/crates/pop-telemetry/src/lib.rs @@ -0,0 +1,109 @@ +use reqwest::Client; +use serde_json::{json, Value}; +use thiserror::Error; + +const WEBSITE_ID: &str = "3da3a7d3-0d51-4f23-a4e0-5e3f7f9442c8"; +const CLI_VERSION: &str = "v1.0.0"; +const ENDPOINT_POSTFIX: &str = "/api/send"; +const RETRY_LIMIT: u8 = 1; + +#[derive(Error, Debug)] +pub enum TelemetryError { + #[error("a reqwest error occurred: {0}")] + NetworkError(reqwest::Error), + #[error("opt-in is not set, can not report metrics")] + NotOptedIn, +} + +type Result = std::result::Result; + +struct Telemetry { + endpoint: String, + opt_in: bool, + retry_limit: u8, + client: Client, +} + +impl Telemetry { + fn new() -> Self { + // environment variable `POP_TELEMETRY_ENDPOINT` is evaluated at compile time + let endpoint = option_env!("POP_TELEMETRY_ENDPOINT").unwrap_or("http://127.0.0.1:3000"); + let mut endpoint: String = endpoint.to_string(); + endpoint.push_str(ENDPOINT_POSTFIX); + + let client = reqwest::Client::new(); + let opt_in = Self::check_opt_in(); + let retry_limit = RETRY_LIMIT; + + Telemetry { endpoint, opt_in, retry_limit, client } + } + + fn check_opt_in() -> bool { + // TODO + true + } + + async fn send_json(&self, payload: Value) -> Result<()> { + if !self.opt_in { + return Err(TelemetryError::NotOptedIn); + } + + let request_builder = self.client.post(&self.endpoint); + + let response = request_builder + .json(&payload) + .send() + .await + .map_err(TelemetryError::NetworkError); + + println!("{:#?}", response); + + Ok(()) + } +} + +pub async fn record_cli_used() -> Result<()> { + let tel = Telemetry::new(); + + let payload = generate_payload("cli", CLI_VERSION, "/", WEBSITE_ID, "", json!({})); + + let res = tel.send_json(payload).await; + log::debug!("send_cli_used result: {:?}", res); + + Ok(()) +} + +pub async fn record_cli_command(command_name: &str, data: Value) -> Result<()> { + let tel = Telemetry::new(); + + let payload = generate_payload("cli", CLI_VERSION, "/", WEBSITE_ID, command_name, data); + + let res = tel.send_json(payload).await?; + log::debug!("send_cli_used result: {:?}", res); + + Ok(()) +} + +fn generate_payload( + hostname: &str, + title: &str, + url: &str, + website_id: &str, + event_name: &str, + data: Value, +) -> Value { + json!({ + "payload": { + "hostname": hostname, + "language": "en-US", + "referrer": "", + "screen": "1920x1080", + "title": title, + "url": url, + "website": website_id, + "name": event_name, + "data": data + }, + "type": "event" + }) +} From 29789044fa7c631f7d5e83396b5cacf601e340a0 Mon Sep 17 00:00:00 2001 From: Peter White Date: Thu, 2 May 2024 14:04:21 -0600 Subject: [PATCH 02/18] feat(telemetry): opt-in handler, better error handling and telemetry usage --- Cargo.lock | 3 + Cargo.toml | 2 + crates/pop-cli/Cargo.toml | 5 +- crates/pop-cli/src/commands/build/contract.rs | 2 +- .../pop-cli/src/commands/build/parachain.rs | 2 +- crates/pop-cli/src/commands/call/contract.rs | 2 +- crates/pop-cli/src/commands/new/contract.rs | 5 +- crates/pop-cli/src/commands/new/pallet.rs | 5 +- crates/pop-cli/src/commands/test/contract.rs | 4 +- crates/pop-cli/src/commands/up/contract.rs | 5 +- crates/pop-cli/src/commands/up/parachain.rs | 6 +- crates/pop-cli/src/main.rs | 24 +++- crates/pop-telemetry/Cargo.toml | 4 +- crates/pop-telemetry/src/lib.rs | 118 ++++++++++++++---- 14 files changed, 137 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0af67527..bb0ff1dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5488,6 +5488,7 @@ dependencies = [ "pop-parachains", "pop-telemetry", "predicates", + "serde", "serde_json", "sp-core 30.0.0", "sp-weights", @@ -5548,9 +5549,11 @@ dependencies = [ name = "pop-telemetry" version = "0.1.0" dependencies = [ + "dirs", "hostname 0.4.0", "log", "reqwest", + "serde", "serde_json", "thiserror", "tokio", diff --git a/Cargo.toml b/Cargo.toml index f70bf318..36bf1232 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,8 @@ tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } url = { version = "2.5" } hostname = "0.4.0" log = "0.4.20" +dirs = { version = "5.0" } + # contracts subxt-signer = { version = "0.34.0", features = ["subxt", "sr25519"] } diff --git a/crates/pop-cli/Cargo.toml b/crates/pop-cli/Cargo.toml index 81ba3356..15390980 100644 --- a/crates/pop-cli/Cargo.toml +++ b/crates/pop-cli/Cargo.toml @@ -14,6 +14,7 @@ duct.workspace = true tempfile.workspace = true url.workspace = true tokio.workspace = true +serde.workspace = true serde_json.workspace = true # pop-cli @@ -30,11 +31,11 @@ sp-weights = { workspace = true, optional = true } # parachains pop-parachains = { path = "../pop-parachains", optional = true } -dirs = { version = "5.0", optional = true } +dirs = { workspace = true, optional = true } git2.workspace = true # telemetry -pop-telemetry = { path = "../pop-telemetry"} +pop-telemetry = { path = "../pop-telemetry" } [dev-dependencies] assert_cmd = "2.0.14" diff --git a/crates/pop-cli/src/commands/build/contract.rs b/crates/pop-cli/src/commands/build/contract.rs index 5df36643..d9702c76 100644 --- a/crates/pop-cli/src/commands/build/contract.rs +++ b/crates/pop-cli/src/commands/build/contract.rs @@ -20,7 +20,7 @@ impl BuildContractCommand { clear_screen()?; intro(format!("{}: Building a contract", style(" Pop CLI ").black().on_magenta()))?; - let _ = tokio::spawn(pop_telemetry::record_cli_command( + tokio::spawn(pop_telemetry::record_cli_command( "build", serde_json::json!({"contract": ""}), )); diff --git a/crates/pop-cli/src/commands/build/parachain.rs b/crates/pop-cli/src/commands/build/parachain.rs index ea667e81..6e790611 100644 --- a/crates/pop-cli/src/commands/build/parachain.rs +++ b/crates/pop-cli/src/commands/build/parachain.rs @@ -21,7 +21,7 @@ impl BuildParachainCommand { clear_screen()?; intro(format!("{}: Building a parachain", style(" Pop CLI ").black().on_magenta()))?; - let _ = tokio::spawn(pop_telemetry::record_cli_command( + tokio::spawn(pop_telemetry::record_cli_command( "build", serde_json::json!({"parachain": ""}), )); diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index 6b45ea47..a889c1ff 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -57,7 +57,7 @@ impl CallContractCommand { pub(crate) async fn execute(&self) -> anyhow::Result<()> { clear_screen()?; intro(format!("{}: Calling a contract", style(" Pop CLI ").black().on_magenta()))?; - let _ = tokio::spawn(pop_telemetry::record_cli_command( + tokio::spawn(pop_telemetry::record_cli_command( "call", serde_json::json!({"contract": ""}), )); diff --git a/crates/pop-cli/src/commands/new/contract.rs b/crates/pop-cli/src/commands/new/contract.rs index 572e2ccf..6049e595 100644 --- a/crates/pop-cli/src/commands/new/contract.rs +++ b/crates/pop-cli/src/commands/new/contract.rs @@ -51,11 +51,10 @@ impl NewContractCommand { spinner.start("Generating contract..."); create_smart_contract(&self.name, contract_path.as_path())?; - let _ = tokio::spawn(pop_telemetry::record_cli_command( + tokio::spawn(pop_telemetry::record_cli_command( "new", serde_json::json!({"contract": "default"}), - )) - .await; + )); spinner.stop("Smart contract created!"); outro(format!("cd into \"{}\" and enjoy hacking! 🚀", contract_path.display()))?; diff --git a/crates/pop-cli/src/commands/new/pallet.rs b/crates/pop-cli/src/commands/new/pallet.rs index 2372f444..403bcc7e 100644 --- a/crates/pop-cli/src/commands/new/pallet.rs +++ b/crates/pop-cli/src/commands/new/pallet.rs @@ -56,11 +56,10 @@ impl NewPalletCommand { }, )?; - let _ = tokio::spawn(pop_telemetry::record_cli_command( + tokio::spawn(pop_telemetry::record_cli_command( "new", serde_json::json!({"pallet": "template"}), - )) - .await; + )); spinner.stop("Generation complete"); outro(format!("cd into \"{}\" and enjoy hacking! 🚀", &self.name))?; diff --git a/crates/pop-cli/src/commands/test/contract.rs b/crates/pop-cli/src/commands/test/contract.rs index edb6f1ef..5948ac9f 100644 --- a/crates/pop-cli/src/commands/test/contract.rs +++ b/crates/pop-cli/src/commands/test/contract.rs @@ -26,7 +26,7 @@ impl TestContractCommand { style(" Pop CLI ").black().on_magenta() ))?; - let _ = tokio::spawn(pop_telemetry::record_cli_command( + tokio::spawn(pop_telemetry::record_cli_command( "test", serde_json::json!({"contract": "e2e"}), )); @@ -35,7 +35,7 @@ impl TestContractCommand { outro("End-to-end testing complete")?; } else { intro(format!("{}: Starting unit tests", style(" Pop CLI ").black().on_magenta()))?; - let _ = tokio::spawn(pop_telemetry::record_cli_command( + tokio::spawn(pop_telemetry::record_cli_command( "test", serde_json::json!({"contract": "unit"}), )); diff --git a/crates/pop-cli/src/commands/up/contract.rs b/crates/pop-cli/src/commands/up/contract.rs index aa9ed285..57a7946b 100644 --- a/crates/pop-cli/src/commands/up/contract.rs +++ b/crates/pop-cli/src/commands/up/contract.rs @@ -56,10 +56,7 @@ impl UpContractCommand { clear_screen()?; intro(format!("{}: Deploy a smart contract", style(" Pop CLI ").black().on_magenta()))?; - let _ = tokio::spawn(pop_telemetry::record_cli_command( - "up", - serde_json::json!({"contract": ""}), - )); + tokio::spawn(pop_telemetry::record_cli_command("up", serde_json::json!({"contract": ""}))); let instantiate_exec = set_up_deployment(UpOpts { path: self.path.clone(), diff --git a/crates/pop-cli/src/commands/up/parachain.rs b/crates/pop-cli/src/commands/up/parachain.rs index 231ae23d..077e5eac 100644 --- a/crates/pop-cli/src/commands/up/parachain.rs +++ b/crates/pop-cli/src/commands/up/parachain.rs @@ -31,11 +31,7 @@ impl ZombienetCommand { clear_screen()?; intro(format!("{}: Deploy a parachain", style(" Pop CLI ").black().on_magenta()))?; - let _ = tokio::spawn(pop_telemetry::record_cli_command( - "up", - serde_json::json!({"parachain": ""}), - )) - .await; + tokio::spawn(pop_telemetry::record_cli_command("up", serde_json::json!({"parachain": ""}))); set_theme(Theme); // Parse arguments diff --git a/crates/pop-cli/src/main.rs b/crates/pop-cli/src/main.rs index 01315d5a..c9bd8774 100644 --- a/crates/pop-cli/src/main.rs +++ b/crates/pop-cli/src/main.rs @@ -7,6 +7,7 @@ mod style; use anyhow::{anyhow, Result}; use clap::{Parser, Subcommand}; +use cliclack::log; use std::{fs::create_dir_all, path::PathBuf}; #[derive(Parser)] @@ -38,10 +39,28 @@ enum Commands { Test(commands::test::TestArgs), } +fn init_config() -> Result<()> { + match pop_telemetry::write_default_config() { + Ok(maybe_path) => { + if let Some(path) = maybe_path { + log::info(format!("Initialized config file at {}", &path.to_str().unwrap()))?; + } + }, + Err(err) => { + log::warning(format!( + "Unable to initialize config file, continuing... {}", + err.to_string() + ))?; + }, + } + Ok(()) +} + #[tokio::main] async fn main() -> Result<()> { - // TODO: use tokio spawn - let _ = pop_telemetry::record_cli_used().await; + init_config()?; + + tokio::spawn(pop_telemetry::record_cli_used()); let cli = Cli::parse(); match cli.command { @@ -75,7 +94,6 @@ async fn main() -> Result<()> { }, } } - #[cfg(feature = "parachain")] fn cache() -> Result { let cache_path = dirs::cache_dir() diff --git a/crates/pop-telemetry/Cargo.toml b/crates/pop-telemetry/Cargo.toml index f042ff82..091887be 100644 --- a/crates/pop-telemetry/Cargo.toml +++ b/crates/pop-telemetry/Cargo.toml @@ -10,4 +10,6 @@ url.workspace = true reqwest.workspace = true serde_json.workspace = true hostname.workspace = true -log.workspace = true \ No newline at end of file +log.workspace = true +dirs = { workspace = true } +serde.workspace = true diff --git a/crates/pop-telemetry/src/lib.rs b/crates/pop-telemetry/src/lib.rs index c72f439d..3ceec4c3 100644 --- a/crates/pop-telemetry/src/lib.rs +++ b/crates/pop-telemetry/src/lib.rs @@ -1,11 +1,14 @@ use reqwest::Client; +use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; +use std::fs::{create_dir_all, File}; +use std::io; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; use thiserror::Error; const WEBSITE_ID: &str = "3da3a7d3-0d51-4f23-a4e0-5e3f7f9442c8"; -const CLI_VERSION: &str = "v1.0.0"; -const ENDPOINT_POSTFIX: &str = "/api/send"; -const RETRY_LIMIT: u8 = 1; +const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); #[derive(Error, Debug)] pub enum TelemetryError { @@ -13,6 +16,12 @@ pub enum TelemetryError { NetworkError(reqwest::Error), #[error("opt-in is not set, can not report metrics")] NotOptedIn, + #[error("unable to find config file")] + ConfigFileNotFound, + #[error("io error occurred: {0}")] + IO(io::Error), + #[error("serialization failed: {0}")] + SerializeFailed(String), } type Result = std::result::Result; @@ -20,29 +29,48 @@ type Result = std::result::Result; struct Telemetry { endpoint: String, opt_in: bool, - retry_limit: u8, client: Client, } impl Telemetry { - fn new() -> Self { - // environment variable `POP_TELEMETRY_ENDPOINT` is evaluated at compile time - let endpoint = option_env!("POP_TELEMETRY_ENDPOINT").unwrap_or("http://127.0.0.1:3000"); - let mut endpoint: String = endpoint.to_string(); - endpoint.push_str(ENDPOINT_POSTFIX); - + fn new(endpoint: String, config_path: PathBuf) -> Self { let client = reqwest::Client::new(); - let opt_in = Self::check_opt_in(); - let retry_limit = RETRY_LIMIT; + let opt_in = Self::check_opt_in(&config_path); - Telemetry { endpoint, opt_in, retry_limit, client } + Telemetry { endpoint, opt_in, client } } - fn check_opt_in() -> bool { - // TODO - true + fn check_opt_in(config_file_path: &PathBuf) -> bool { + let mut file = File::open(config_file_path) + .map_err(|err| { + log::debug!("{}", err.to_string()); + return false; + }) + .expect("error mapped above"); + + let mut config_json = String::new(); + file.read_to_string(&mut config_json) + .map_err(|err| { + log::debug!("{}", err.to_string()); + return false; + }) + .expect("error mapped above"); + + let config: Config = serde_json::from_str(&config_json) + .map_err(|err| { + log::debug!("{}", err.to_string()); + return false; + }) + .expect("error mapped above"); + + config.opt_in.allow } + /// Send JSON payload to saved api endpoint. + /// Will return error and not send anything if opt-in is false. + /// Will return error from reqwest if the sending fails. + /// It sends message only once as "best effort". There is no retry on error + /// in order to keep overhead to a minimal. async fn send_json(&self, payload: Value) -> Result<()> { if !self.opt_in { return Err(TelemetryError::NotOptedIn); @@ -50,22 +78,24 @@ impl Telemetry { let request_builder = self.client.post(&self.endpoint); - let response = request_builder + request_builder .json(&payload) .send() .await - .map_err(TelemetryError::NetworkError); - - println!("{:#?}", response); + .map_err(TelemetryError::NetworkError)?; Ok(()) } } pub async fn record_cli_used() -> Result<()> { - let tel = Telemetry::new(); + // environment variable `POP_TELEMETRY_ENDPOINT` is evaluated at compile time + let endpoint = + option_env!("POP_TELEMETRY_ENDPOINT").unwrap_or("http://127.0.0.1:3000/api/send"); + + let tel = Telemetry::new(endpoint.into(), config_file_path()?); - let payload = generate_payload("cli", CLI_VERSION, "/", WEBSITE_ID, "", json!({})); + let payload = generate_payload("cli", CARGO_PKG_VERSION, "/", WEBSITE_ID, "", json!({})); let res = tel.send_json(payload).await; log::debug!("send_cli_used result: {:?}", res); @@ -74,9 +104,13 @@ pub async fn record_cli_used() -> Result<()> { } pub async fn record_cli_command(command_name: &str, data: Value) -> Result<()> { - let tel = Telemetry::new(); + // environment variable `POP_TELEMETRY_ENDPOINT` is evaluated at *compile* time + let endpoint = + option_env!("POP_TELEMETRY_ENDPOINT").unwrap_or("http://127.0.0.1:3000/api/send"); - let payload = generate_payload("cli", CLI_VERSION, "/", WEBSITE_ID, command_name, data); + let tel = Telemetry::new(endpoint.into(), config_file_path()?); + + let payload = generate_payload("cli", CARGO_PKG_VERSION, "/", WEBSITE_ID, command_name, data); let res = tel.send_json(payload).await?; log::debug!("send_cli_used result: {:?}", res); @@ -84,6 +118,42 @@ pub async fn record_cli_command(command_name: &str, data: Value) -> Result<()> { Ok(()) } +#[derive(Serialize, Deserialize, Debug)] +struct OptIn { + allow: bool, + version: String, +} +#[derive(Serialize, Deserialize, Debug)] +pub struct Config { + opt_in: OptIn, +} +pub fn config_file_path() -> Result { + let config_path = dirs::config_dir().ok_or(TelemetryError::ConfigFileNotFound)?.join("pop"); + // Creates pop dir if needed + create_dir_all(config_path.as_path()).map_err(|err| TelemetryError::IO(err))?; + Ok(config_path.join("config.json")) +} + +pub fn write_default_config() -> Result> { + let config_path = config_file_path()?; + if !Path::new(&config_path).exists() { + let default_config = + Config { opt_in: OptIn { allow: true, version: CARGO_PKG_VERSION.to_string() } }; + + let default_config_json = serde_json::to_string_pretty(&default_config) + .map_err(|err| TelemetryError::SerializeFailed(err.to_string()))?; + + let mut file = File::create(&config_path).map_err(|err| TelemetryError::IO(err))?; + file.write_all(default_config_json.as_bytes()) + .map_err(|err| TelemetryError::IO(err))?; + } else { + // if the file already existed, return None + return Ok(None); + } + + Ok(Some(config_path)) +} + fn generate_payload( hostname: &str, title: &str, From 4d48e5495cbe15e03a61f328c490fc36cc7314e6 Mon Sep 17 00:00:00 2001 From: Peter White Date: Fri, 3 May 2024 00:25:04 -0600 Subject: [PATCH 03/18] feat(telemetry): add reporting for errors --- crates/pop-cli/src/main.rs | 86 +++++++++++++++++++++++++++++---- crates/pop-telemetry/src/lib.rs | 2 +- 2 files changed, 77 insertions(+), 11 deletions(-) diff --git a/crates/pop-cli/src/main.rs b/crates/pop-cli/src/main.rs index c9bd8774..ee8c3597 100644 --- a/crates/pop-cli/src/main.rs +++ b/crates/pop-cli/src/main.rs @@ -9,6 +9,7 @@ use anyhow::{anyhow, Result}; use clap::{Parser, Subcommand}; use cliclack::log; use std::{fs::create_dir_all, path::PathBuf}; +use tokio::task::JoinHandle; #[derive(Parser)] #[command(author, version, about, styles=style::get_styles())] @@ -60,39 +61,104 @@ fn init_config() -> Result<()> { async fn main() -> Result<()> { init_config()?; + // handle for await not used here as telemetry should complete before any of the commands do. tokio::spawn(pop_telemetry::record_cli_used()); + // If error occurs, this will be used to ensure error telemetry is complete before destructing. + // This await handle is used as the error will not have sufficient time to report before destruction. + let mut tel_error_handle: Option>> = None; + let cli = Cli::parse(); - match cli.command { + let res = match cli.command { Commands::New(args) => Ok(match &args.command { #[cfg(feature = "parachain")] - commands::new::NewCommands::Parachain(cmd) => cmd.execute().await?, + commands::new::NewCommands::Parachain(cmd) => cmd.execute().await.map_err(|err| { + tel_error_handle = Some(tokio::spawn(pop_telemetry::record_cli_command( + "error", + serde_json::json!({"new": "parachain"}), + ))); + err + })?, #[cfg(feature = "parachain")] - commands::new::NewCommands::Pallet(cmd) => cmd.execute().await?, + commands::new::NewCommands::Pallet(cmd) => cmd.execute().await.map_err(|err| { + tel_error_handle = Some(tokio::spawn(pop_telemetry::record_cli_command( + "error", + serde_json::json!({"new": "pallet"}), + ))); + err + })?, #[cfg(feature = "contract")] - commands::new::NewCommands::Contract(cmd) => cmd.execute().await?, + commands::new::NewCommands::Contract(cmd) => cmd.execute().await.map_err(|err| { + tel_error_handle = Some(tokio::spawn(pop_telemetry::record_cli_command( + "error", + serde_json::json!({"new": "contract"}), + ))); + err + })?, }), Commands::Build(args) => match &args.command { #[cfg(feature = "parachain")] - commands::build::BuildCommands::Parachain(cmd) => cmd.execute(), + commands::build::BuildCommands::Parachain(cmd) => cmd.execute().map_err(|err| { + tel_error_handle = Some(tokio::spawn(pop_telemetry::record_cli_command( + "error", + serde_json::json!({"build": "parachain"}), + ))); + err + }), #[cfg(feature = "contract")] - commands::build::BuildCommands::Contract(cmd) => cmd.execute(), + commands::build::BuildCommands::Contract(cmd) => cmd.execute().map_err(|err| { + tel_error_handle = Some(tokio::spawn(pop_telemetry::record_cli_command( + "error", + serde_json::json!({"build": "contract"}), + ))); + err + }), }, #[cfg(feature = "contract")] Commands::Call(args) => Ok(match &args.command { - commands::call::CallCommands::Contract(cmd) => cmd.execute().await?, + commands::call::CallCommands::Contract(cmd) => cmd.execute().await.map_err(|err| { + tel_error_handle = Some(tokio::spawn(pop_telemetry::record_cli_command( + "error", + serde_json::json!({"call": "contract"}), + ))); + err + })?, }), Commands::Up(args) => Ok(match &args.command { #[cfg(feature = "parachain")] - commands::up::UpCommands::Parachain(cmd) => cmd.execute().await?, + commands::up::UpCommands::Parachain(cmd) => cmd.execute().await.map_err(|err| { + tel_error_handle = Some(tokio::spawn(pop_telemetry::record_cli_command( + "error", + serde_json::json!({"up": "parachain"}), + ))); + err + })?, #[cfg(feature = "contract")] - commands::up::UpCommands::Contract(cmd) => cmd.execute().await?, + commands::up::UpCommands::Contract(cmd) => cmd.execute().await.map_err(|err| { + tel_error_handle = Some(tokio::spawn(pop_telemetry::record_cli_command( + "error", + serde_json::json!({"up": "contract"}), + ))); + err + })?, }), #[cfg(feature = "contract")] Commands::Test(args) => match &args.command { - commands::test::TestCommands::Contract(cmd) => cmd.execute(), + commands::test::TestCommands::Contract(cmd) => cmd.execute().map_err(|err| { + tel_error_handle = Some(tokio::spawn(pop_telemetry::record_cli_command( + "error", + serde_json::json!({"test": "contract"}), + ))); + err + }), }, + }; + + if let Some(handle) = tel_error_handle { + let _ = handle.await; } + + res } #[cfg(feature = "parachain")] fn cache() -> Result { diff --git a/crates/pop-telemetry/src/lib.rs b/crates/pop-telemetry/src/lib.rs index 3ceec4c3..e5b1e404 100644 --- a/crates/pop-telemetry/src/lib.rs +++ b/crates/pop-telemetry/src/lib.rs @@ -24,7 +24,7 @@ pub enum TelemetryError { SerializeFailed(String), } -type Result = std::result::Result; +pub type Result = std::result::Result; struct Telemetry { endpoint: String, From d8e2c9b558498033134169fb66b115b9f73373e5 Mon Sep 17 00:00:00 2001 From: Peter White Date: Fri, 3 May 2024 14:36:02 -0600 Subject: [PATCH 04/18] refactor(telemetry): reduce duplicated code, add comments, and general refactoring -- incomplete checkpoint --- Cargo.toml | 8 +- crates/pop-cli/Cargo.toml | 6 +- crates/pop-cli/src/commands/build/contract.rs | 5 - .../pop-cli/src/commands/build/parachain.rs | 5 - crates/pop-cli/src/commands/call/contract.rs | 4 - crates/pop-cli/src/commands/new/contract.rs | 5 - crates/pop-cli/src/commands/new/pallet.rs | 5 - crates/pop-cli/src/commands/up/contract.rs | 2 - crates/pop-cli/src/commands/up/parachain.rs | 2 - crates/pop-cli/src/main.rs | 158 ++++++++---------- crates/pop-telemetry/Cargo.toml | 12 +- crates/pop-telemetry/src/lib.rs | 60 ++++--- 12 files changed, 123 insertions(+), 149 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 36bf1232..96d4bb34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,15 +14,15 @@ members = ["crates/*"] [workspace.dependencies] anyhow = "1.0" -thiserror = "1.0.58" +dirs = { version = "5.0" } duct = "0.13" git2 = "0.18" +hostname = "0.4.0" +log = "0.4.20" tempfile = "3.8" +thiserror = "1.0.58" tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } url = { version = "2.5" } -hostname = "0.4.0" -log = "0.4.20" -dirs = { version = "5.0" } # contracts diff --git a/crates/pop-cli/Cargo.toml b/crates/pop-cli/Cargo.toml index 15390980..635e93b0 100644 --- a/crates/pop-cli/Cargo.toml +++ b/crates/pop-cli/Cargo.toml @@ -11,11 +11,11 @@ path = "src/main.rs" [dependencies] anyhow.workspace = true duct.workspace = true -tempfile.workspace = true -url.workspace = true -tokio.workspace = true serde.workspace = true serde_json.workspace = true +tempfile.workspace = true +tokio.workspace = true +url.workspace = true # pop-cli clap.workspace = true diff --git a/crates/pop-cli/src/commands/build/contract.rs b/crates/pop-cli/src/commands/build/contract.rs index d9702c76..3e32d590 100644 --- a/crates/pop-cli/src/commands/build/contract.rs +++ b/crates/pop-cli/src/commands/build/contract.rs @@ -20,11 +20,6 @@ impl BuildContractCommand { clear_screen()?; intro(format!("{}: Building a contract", style(" Pop CLI ").black().on_magenta()))?; - tokio::spawn(pop_telemetry::record_cli_command( - "build", - serde_json::json!({"contract": ""}), - )); - set_theme(Theme); let result_build = build_smart_contract(&self.path)?; diff --git a/crates/pop-cli/src/commands/build/parachain.rs b/crates/pop-cli/src/commands/build/parachain.rs index 6e790611..db65627f 100644 --- a/crates/pop-cli/src/commands/build/parachain.rs +++ b/crates/pop-cli/src/commands/build/parachain.rs @@ -21,11 +21,6 @@ impl BuildParachainCommand { clear_screen()?; intro(format!("{}: Building a parachain", style(" Pop CLI ").black().on_magenta()))?; - tokio::spawn(pop_telemetry::record_cli_command( - "build", - serde_json::json!({"parachain": ""}), - )); - set_theme(Theme); build_parachain(&self.path)?; diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index a889c1ff..a91387b5 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -57,10 +57,6 @@ impl CallContractCommand { pub(crate) async fn execute(&self) -> anyhow::Result<()> { clear_screen()?; intro(format!("{}: Calling a contract", style(" Pop CLI ").black().on_magenta()))?; - tokio::spawn(pop_telemetry::record_cli_command( - "call", - serde_json::json!({"contract": ""}), - )); set_theme(Theme); diff --git a/crates/pop-cli/src/commands/new/contract.rs b/crates/pop-cli/src/commands/new/contract.rs index 6049e595..78dc3edf 100644 --- a/crates/pop-cli/src/commands/new/contract.rs +++ b/crates/pop-cli/src/commands/new/contract.rs @@ -51,11 +51,6 @@ impl NewContractCommand { spinner.start("Generating contract..."); create_smart_contract(&self.name, contract_path.as_path())?; - tokio::spawn(pop_telemetry::record_cli_command( - "new", - serde_json::json!({"contract": "default"}), - )); - spinner.stop("Smart contract created!"); outro(format!("cd into \"{}\" and enjoy hacking! 🚀", contract_path.display()))?; Ok(()) diff --git a/crates/pop-cli/src/commands/new/pallet.rs b/crates/pop-cli/src/commands/new/pallet.rs index 403bcc7e..03c997ec 100644 --- a/crates/pop-cli/src/commands/new/pallet.rs +++ b/crates/pop-cli/src/commands/new/pallet.rs @@ -56,11 +56,6 @@ impl NewPalletCommand { }, )?; - tokio::spawn(pop_telemetry::record_cli_command( - "new", - serde_json::json!({"pallet": "template"}), - )); - spinner.stop("Generation complete"); outro(format!("cd into \"{}\" and enjoy hacking! 🚀", &self.name))?; Ok(()) diff --git a/crates/pop-cli/src/commands/up/contract.rs b/crates/pop-cli/src/commands/up/contract.rs index 57a7946b..aa9b8e3a 100644 --- a/crates/pop-cli/src/commands/up/contract.rs +++ b/crates/pop-cli/src/commands/up/contract.rs @@ -56,8 +56,6 @@ impl UpContractCommand { clear_screen()?; intro(format!("{}: Deploy a smart contract", style(" Pop CLI ").black().on_magenta()))?; - tokio::spawn(pop_telemetry::record_cli_command("up", serde_json::json!({"contract": ""}))); - let instantiate_exec = set_up_deployment(UpOpts { path: self.path.clone(), constructor: self.constructor.clone(), diff --git a/crates/pop-cli/src/commands/up/parachain.rs b/crates/pop-cli/src/commands/up/parachain.rs index 077e5eac..6a6774cc 100644 --- a/crates/pop-cli/src/commands/up/parachain.rs +++ b/crates/pop-cli/src/commands/up/parachain.rs @@ -31,8 +31,6 @@ impl ZombienetCommand { clear_screen()?; intro(format!("{}: Deploy a parachain", style(" Pop CLI ").black().on_magenta()))?; - tokio::spawn(pop_telemetry::record_cli_command("up", serde_json::json!({"parachain": ""}))); - set_theme(Theme); // Parse arguments let cache = crate::cache()?; diff --git a/crates/pop-cli/src/main.rs b/crates/pop-cli/src/main.rs index ee8c3597..c13e5455 100644 --- a/crates/pop-cli/src/main.rs +++ b/crates/pop-cli/src/main.rs @@ -8,8 +8,11 @@ mod style; use anyhow::{anyhow, Result}; use clap::{Parser, Subcommand}; use cliclack::log; +use commands::*; +use pop_telemetry::{record_cli_command, record_cli_used}; +use serde_json::json; use std::{fs::create_dir_all, path::PathBuf}; -use tokio::task::JoinHandle; +use tokio::{spawn, task::JoinHandle}; #[derive(Parser)] #[command(author, version, about, styles=style::get_styles())] @@ -40,122 +43,86 @@ enum Commands { Test(commands::test::TestArgs), } -fn init_config() -> Result<()> { - match pop_telemetry::write_default_config() { - Ok(maybe_path) => { - if let Some(path) = maybe_path { - log::info(format!("Initialized config file at {}", &path.to_str().unwrap()))?; - } - }, - Err(err) => { - log::warning(format!( - "Unable to initialize config file, continuing... {}", - err.to_string() - ))?; - }, - } - Ok(()) -} - #[tokio::main] async fn main() -> Result<()> { init_config()?; // handle for await not used here as telemetry should complete before any of the commands do. - tokio::spawn(pop_telemetry::record_cli_used()); + // Sends a generic ping saying the CLI was used + spawn(record_cli_used()); - // If error occurs, this will be used to ensure error telemetry is complete before destructing. - // This await handle is used as the error will not have sufficient time to report before destruction. - let mut tel_error_handle: Option>> = None; + // type to represent static telemetry data. I.e., does not contain data dynamically chosen by user + // like in pop new parachain. + let mut tel_data: (&str, &str, &str) = ("", "", ""); let cli = Cli::parse(); let res = match cli.command { Commands::New(args) => Ok(match &args.command { #[cfg(feature = "parachain")] - commands::new::NewCommands::Parachain(cmd) => cmd.execute().await.map_err(|err| { - tel_error_handle = Some(tokio::spawn(pop_telemetry::record_cli_command( - "error", - serde_json::json!({"new": "parachain"}), - ))); - err - })?, + new::NewCommands::Parachain(cmd) => cmd.execute().await?, #[cfg(feature = "parachain")] - commands::new::NewCommands::Pallet(cmd) => cmd.execute().await.map_err(|err| { - tel_error_handle = Some(tokio::spawn(pop_telemetry::record_cli_command( - "error", - serde_json::json!({"new": "pallet"}), - ))); - err - })?, + new::NewCommands::Pallet(cmd) => { + // when there are more pallet selections, this will likely have to move deeper into the stack + tel_data = ("new", "pallet", "template"); + + cmd.execute().await? + }, #[cfg(feature = "contract")] - commands::new::NewCommands::Contract(cmd) => cmd.execute().await.map_err(|err| { - tel_error_handle = Some(tokio::spawn(pop_telemetry::record_cli_command( - "error", - serde_json::json!({"new": "contract"}), - ))); - err - })?, + new::NewCommands::Contract(cmd) => { + // When more contract selections are added this will likely need to go deeped in the stack + tel_data = ("new", "contract", "default"); + + cmd.execute().await? + }, }), Commands::Build(args) => match &args.command { #[cfg(feature = "parachain")] - commands::build::BuildCommands::Parachain(cmd) => cmd.execute().map_err(|err| { - tel_error_handle = Some(tokio::spawn(pop_telemetry::record_cli_command( - "error", - serde_json::json!({"build": "parachain"}), - ))); - err - }), + build::BuildCommands::Parachain(cmd) => { + tel_data = ("build", "parachain", ""); + + cmd.execute() + }, #[cfg(feature = "contract")] - commands::build::BuildCommands::Contract(cmd) => cmd.execute().map_err(|err| { - tel_error_handle = Some(tokio::spawn(pop_telemetry::record_cli_command( - "error", - serde_json::json!({"build": "contract"}), - ))); - err - }), + build::BuildCommands::Contract(cmd) => { + tel_data = ("build", "contract", ""); + + cmd.execute() + }, }, #[cfg(feature = "contract")] Commands::Call(args) => Ok(match &args.command { - commands::call::CallCommands::Contract(cmd) => cmd.execute().await.map_err(|err| { - tel_error_handle = Some(tokio::spawn(pop_telemetry::record_cli_command( - "error", - serde_json::json!({"call": "contract"}), - ))); - err - })?, + call::CallCommands::Contract(cmd) => { + tel_data = ("call", "contract", ""); + + cmd.execute().await? + }, }), Commands::Up(args) => Ok(match &args.command { #[cfg(feature = "parachain")] - commands::up::UpCommands::Parachain(cmd) => cmd.execute().await.map_err(|err| { - tel_error_handle = Some(tokio::spawn(pop_telemetry::record_cli_command( - "error", - serde_json::json!({"up": "parachain"}), - ))); - err - })?, + up::UpCommands::Parachain(cmd) => { + tel_data = ("up", "parachain", ""); + + cmd.execute().await? + }, #[cfg(feature = "contract")] - commands::up::UpCommands::Contract(cmd) => cmd.execute().await.map_err(|err| { - tel_error_handle = Some(tokio::spawn(pop_telemetry::record_cli_command( - "error", - serde_json::json!({"up": "contract"}), - ))); - err - })?, + up::UpCommands::Contract(cmd) => { + tel_data = ("up", "contract", ""); + + cmd.execute().await? + }, }), #[cfg(feature = "contract")] Commands::Test(args) => match &args.command { - commands::test::TestCommands::Contract(cmd) => cmd.execute().map_err(|err| { - tel_error_handle = Some(tokio::spawn(pop_telemetry::record_cli_command( - "error", - serde_json::json!({"test": "contract"}), - ))); - err - }), + test::TestCommands::Contract(cmd) => cmd.execute(), }, }; - if let Some(handle) = tel_error_handle { - let _ = handle.await; + let tel_data_handle = spawn(record_cli_command(tel_data.0, json!({tel_data.1: tel_data.2}))); + // Best effort to send on first try, no action if failure + let _ = tel_data_handle.await; + // Send if error + if res.is_err() { + let _ = spawn(record_cli_command("error", json!({tel_data.0: tel_data.1}))).await; } res @@ -169,3 +136,20 @@ fn cache() -> Result { create_dir_all(cache_path.as_path())?; Ok(cache_path) } + +fn init_config() -> Result<()> { + match pop_telemetry::write_default_config() { + Ok(maybe_path) => { + if let Some(path) = maybe_path { + log::info(format!("Initialized config file at {}", &path.to_str().unwrap()))?; + } + }, + Err(err) => { + log::warning(format!( + "Unable to initialize config file, continuing... {}", + err.to_string() + ))?; + }, + } + Ok(()) +} diff --git a/crates/pop-telemetry/Cargo.toml b/crates/pop-telemetry/Cargo.toml index 091887be..be7c1343 100644 --- a/crates/pop-telemetry/Cargo.toml +++ b/crates/pop-telemetry/Cargo.toml @@ -4,12 +4,12 @@ version = "0.1.0" edition = "2021" [dependencies] -thiserror.workspace = true -tokio.workspace = true -url.workspace = true -reqwest.workspace = true -serde_json.workspace = true +dirs = { workspace = true } hostname.workspace = true log.workspace = true -dirs = { workspace = true } +reqwest.workspace = true serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true +tokio.workspace = true +url.workspace = true diff --git a/crates/pop-telemetry/src/lib.rs b/crates/pop-telemetry/src/lib.rs index e5b1e404..e311a75c 100644 --- a/crates/pop-telemetry/src/lib.rs +++ b/crates/pop-telemetry/src/lib.rs @@ -1,10 +1,12 @@ use reqwest::Client; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use std::fs::{create_dir_all, File}; -use std::io; -use std::io::{Read, Write}; -use std::path::{Path, PathBuf}; +use std::{ + fs::{create_dir_all, File}, + io, + io::{Read, Write}, + path::{Path, PathBuf}, +}; use thiserror::Error; const WEBSITE_ID: &str = "3da3a7d3-0d51-4f23-a4e0-5e3f7f9442c8"; @@ -14,21 +16,25 @@ const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); pub enum TelemetryError { #[error("a reqwest error occurred: {0}")] NetworkError(reqwest::Error), + #[error("io error occurred: {0}")] + IO(io::Error), #[error("opt-in is not set, can not report metrics")] NotOptedIn, #[error("unable to find config file")] ConfigFileNotFound, - #[error("io error occurred: {0}")] - IO(io::Error), #[error("serialization failed: {0}")] SerializeFailed(String), } -pub type Result = std::result::Result; +type Result = std::result::Result; struct Telemetry { + // Endpoint to the telemetry API. + // This should include the domain and api path (e.g. localhost:3000/api/send) endpoint: String, + // Has the user opted-in to anonymous telemetry opt_in: bool, + // Reqwest client client: Client, } @@ -88,6 +94,9 @@ impl Telemetry { } } +/// Generically reports that the CLI was used to the telemetry endpoint. +/// There is explicitly no reqwest retries on failure to ensure overhead +/// stays to a minimum. pub async fn record_cli_used() -> Result<()> { // environment variable `POP_TELEMETRY_ENDPOINT` is evaluated at compile time let endpoint = @@ -95,7 +104,7 @@ pub async fn record_cli_used() -> Result<()> { let tel = Telemetry::new(endpoint.into(), config_file_path()?); - let payload = generate_payload("cli", CARGO_PKG_VERSION, "/", WEBSITE_ID, "", json!({})); + let payload = generate_payload("", json!({})); let res = tel.send_json(payload).await; log::debug!("send_cli_used result: {:?}", res); @@ -103,6 +112,12 @@ pub async fn record_cli_used() -> Result<()> { Ok(()) } +/// Reports what CLI command was called to telemetry. +/// +/// parameters: +/// `command_name`: the name of the command entered (new, up, build, etc) +/// `data`: the JSON representation of subcommands. This should never include any user inputted +/// data like a file name. pub async fn record_cli_command(command_name: &str, data: Value) -> Result<()> { // environment variable `POP_TELEMETRY_ENDPOINT` is evaluated at *compile* time let endpoint = @@ -110,7 +125,7 @@ pub async fn record_cli_command(command_name: &str, data: Value) -> Result<()> { let tel = Telemetry::new(endpoint.into(), config_file_path()?); - let payload = generate_payload("cli", CARGO_PKG_VERSION, "/", WEBSITE_ID, command_name, data); + let payload = generate_payload(command_name, data); let res = tel.send_json(payload).await?; log::debug!("send_cli_used result: {:?}", res); @@ -120,13 +135,20 @@ pub async fn record_cli_command(command_name: &str, data: Value) -> Result<()> { #[derive(Serialize, Deserialize, Debug)] struct OptIn { + // did user opt in allow: bool, + // what telemetry version did they opt-in for version: String, } + +/// Type to represent pop cli configuration. +/// This will be written as json to a config.json file. #[derive(Serialize, Deserialize, Debug)] pub struct Config { opt_in: OptIn, } + +/// Returns the configuration file path based on OS's default config directory. pub fn config_file_path() -> Result { let config_path = dirs::config_dir().ok_or(TelemetryError::ConfigFileNotFound)?.join("pop"); // Creates pop dir if needed @@ -134,6 +156,9 @@ pub fn config_file_path() -> Result { Ok(config_path.join("config.json")) } +/// Writes a default config to the configuration file from `config_file_path` +/// If return value is Result(None), this means that the config file already existed +/// and no writing was necessary. Otherwise, the path to the file is returned. pub fn write_default_config() -> Result> { let config_path = config_file_path()?; if !Path::new(&config_path).exists() { @@ -154,23 +179,16 @@ pub fn write_default_config() -> Result> { Ok(Some(config_path)) } -fn generate_payload( - hostname: &str, - title: &str, - url: &str, - website_id: &str, - event_name: &str, - data: Value, -) -> Value { +fn generate_payload(event_name: &str, data: Value) -> Value { json!({ "payload": { - "hostname": hostname, + "hostname": "cli", "language": "en-US", "referrer": "", "screen": "1920x1080", - "title": title, - "url": url, - "website": website_id, + "title": CARGO_PKG_VERSION, + "url": "/", + "website": WEBSITE_ID, "name": event_name, "data": data }, From a648e59113117a9bd67e0e0f0c1c5896eaab2696 Mon Sep 17 00:00:00 2001 From: Peter White Date: Sat, 4 May 2024 00:12:16 -0600 Subject: [PATCH 05/18] refactor(telemetry): refactor new parachain, and telemetry handling --- crates/pop-cli/src/commands/new/parachain.rs | 79 +++++++++++--------- crates/pop-cli/src/commands/test/contract.rs | 17 ++--- crates/pop-cli/src/main.rs | 38 ++++++---- crates/pop-parachains/src/templates.rs | 6 ++ crates/pop-telemetry/src/lib.rs | 2 +- 5 files changed, 82 insertions(+), 60 deletions(-) diff --git a/crates/pop-cli/src/commands/new/parachain.rs b/crates/pop-cli/src/commands/new/parachain.rs index efd2ec72..be2718af 100644 --- a/crates/pop-cli/src/commands/new/parachain.rs +++ b/crates/pop-cli/src/commands/new/parachain.rs @@ -9,8 +9,10 @@ use std::{ use cliclack::{clear_screen, confirm, input, intro, log, outro, outro_cancel, set_theme}; use pop_parachains::{instantiate_template_dir, Config, Git, GitHub, Provider, Release, Template}; +use pop_telemetry::Result as TelResult; +use tokio::task::JoinHandle; -#[derive(Args)] +#[derive(Args, Clone)] pub struct NewParachainCommand { #[arg(help = "Name of the project. If empty assistance in the process will be provided.")] pub(crate) name: Option, @@ -25,6 +27,8 @@ pub struct NewParachainCommand { help = "Template to use: 'base' for Pop and 'cpt' and 'fpt' for Parity templates" )] pub(crate) template: Option