diff --git a/Cargo.lock b/Cargo.lock index cafeef166d..21970cdd78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2695,7 +2695,7 @@ dependencies = [ [[package]] name = "ic-agent" version = "0.39.3" -source = "git+https://github.com/dfinity/agent-rs?rev=9ebf6314ce2fcb36772c7d81d6d414b4628d6101#9ebf6314ce2fcb36772c7d81d6d414b4628d6101" +source = "git+https://github.com/dfinity/agent-rs?rev=7752c33016f708fb84b56ff99d91a7c380a8b550#7752c33016f708fb84b56ff99d91a7c380a8b550" dependencies = [ "arc-swap", "async-channel", @@ -2714,7 +2714,7 @@ dependencies = [ "http 1.2.0", "http-body 1.0.1", "ic-certification 3.0.2", - "ic-transport-types 0.39.3 (git+https://github.com/dfinity/agent-rs?rev=9ebf6314ce2fcb36772c7d81d6d414b4628d6101)", + "ic-transport-types 0.39.3 (git+https://github.com/dfinity/agent-rs?rev=7752c33016f708fb84b56ff99d91a7c380a8b550)", "ic-verify-bls-signature", "k256 0.13.4", "leb128", @@ -3154,7 +3154,7 @@ dependencies = [ [[package]] name = "ic-identity-hsm" version = "0.39.3" -source = "git+https://github.com/dfinity/agent-rs?rev=9ebf6314ce2fcb36772c7d81d6d414b4628d6101#9ebf6314ce2fcb36772c7d81d6d414b4628d6101" +source = "git+https://github.com/dfinity/agent-rs?rev=7752c33016f708fb84b56ff99d91a7c380a8b550#7752c33016f708fb84b56ff99d91a7c380a8b550" dependencies = [ "hex", "ic-agent", @@ -3293,7 +3293,7 @@ dependencies = [ [[package]] name = "ic-transport-types" version = "0.39.3" -source = "git+https://github.com/dfinity/agent-rs?rev=9ebf6314ce2fcb36772c7d81d6d414b4628d6101#9ebf6314ce2fcb36772c7d81d6d414b4628d6101" +source = "git+https://github.com/dfinity/agent-rs?rev=7752c33016f708fb84b56ff99d91a7c380a8b550#7752c33016f708fb84b56ff99d91a7c380a8b550" dependencies = [ "candid", "hex", @@ -3363,7 +3363,7 @@ dependencies = [ [[package]] name = "ic-utils" version = "0.39.3" -source = "git+https://github.com/dfinity/agent-rs?rev=9ebf6314ce2fcb36772c7d81d6d414b4628d6101#9ebf6314ce2fcb36772c7d81d6d414b4628d6101" +source = "git+https://github.com/dfinity/agent-rs?rev=7752c33016f708fb84b56ff99d91a7c380a8b550#7752c33016f708fb84b56ff99d91a7c380a8b550" dependencies = [ "async-trait", "candid", diff --git a/Cargo.toml b/Cargo.toml index 9163ab80a7..3b3168f9ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,11 +25,11 @@ needless_lifetimes = "allow" candid = "0.10.11" candid_parser = "0.1.4" dfx-core = { path = "src/dfx-core", version = "0.1.0" } -ic-agent = { version = "0.39", git = "https://github.com/dfinity/agent-rs", rev = "9ebf6314ce2fcb36772c7d81d6d414b4628d6101" } +ic-agent = { version = "0.39", git = "https://github.com/dfinity/agent-rs", rev = "7752c33016f708fb84b56ff99d91a7c380a8b550" } ic-asset = { path = "src/canisters/frontend/ic-asset", version = "0.22.0" } ic-cdk = "0.13.1" -ic-identity-hsm = { version = "0.39", git = "https://github.com/dfinity/agent-rs", rev = "9ebf6314ce2fcb36772c7d81d6d414b4628d6101" } -ic-utils = { version = "0.39", git = "https://github.com/dfinity/agent-rs", rev = "9ebf6314ce2fcb36772c7d81d6d414b4628d6101" } +ic-identity-hsm = { version = "0.39", git = "https://github.com/dfinity/agent-rs", rev = "7752c33016f708fb84b56ff99d91a7c380a8b550" } +ic-utils = { version = "0.39", git = "https://github.com/dfinity/agent-rs", rev = "7752c33016f708fb84b56ff99d91a7c380a8b550" } aes-gcm = { version = "0.10.3", features = ["std"] } anyhow = "1.0.56" diff --git a/e2e/tests-dfx/telemetry.bash b/e2e/tests-dfx/telemetry.bash index b69e234773..90b1f7ca6a 100755 --- a/e2e/tests-dfx/telemetry.bash +++ b/e2e/tests-dfx/telemetry.bash @@ -6,6 +6,8 @@ setup() { standard_setup dfx_new + + export DFX_TELEMETRY=local } teardown() { @@ -45,7 +47,7 @@ teardown() { assert_eq local } -@test "telemetry is collected" { +@test "command-line args are collected" { local expected_platform log n case "$(uname)" in Darwin) expected_platform=macos;; @@ -53,13 +55,13 @@ teardown() { *) fail 'unknown platform';; esac log=$(dfx info telemetry-log-path) - assert_command env DFX_TELEMETRY=local dfx identity get-principal + assert_command dfx identity get-principal assert_command jq -se 'last | .command == "identity get-principal" and .platform == "'"$expected_platform"'" and .exit_code == 0 and (.parameters | length == 0)' "$log" n=$(jq -sr length "$log") - assert_command_fail env DFX_TELEMETRY=local DFX_NETWORK=ic dfx identity get-platypus + assert_command_fail env DFX_NETWORK=ic dfx identity get-platypus assert_command jq -se "length == $n" "$log" - assert_command_fail env DFX_TELEMETRY=local DFX_NETWORK=ic dfx identity get-principal --identity platypus + assert_command_fail env DFX_NETWORK=ic dfx identity get-principal --identity platypus assert_command jq -se 'length == '$((n+1))' and (last | .command == "identity get-principal" and .exit_code == 255 and (.parameters | any(.name == "network" and .source == "environment") and any(.name == "identity" and .source == "command-line")))' "$log" @@ -69,7 +71,7 @@ teardown() { local log log=$(dfx info telemetry-log-path) assert_command dfx extension install nns --version 0.3.1 - assert_command env DFX_TELEMETRY=local dfx nns import + assert_command dfx nns import assert_command jq -se 'last | .command == "extension run" and (.parameters | any(.name == "name"))' "$log" } @@ -78,8 +80,85 @@ teardown() { log=$(dfx info telemetry-log-path) dfx identity get-principal # initialize it first for _ in {0..100}; do - assert_command env DFX_TELEMETRY=local dfx identity get-principal & + assert_command dfx identity get-principal & done wait assert_command jq -se '.[-101:-1] | all(.command == "identity get-principal") and length == 100' "$log" } + +@test "the last replica error is collected" { + dfx_new_assets + local log wallet + log=$(dfx info telemetry-log-path) + # explicit call, known canister (ICP ledger) + dfx_start + assert_command_fail dfx canister call ryjl3-tyaaa-aaaaa-aaaba-cai name + assert_command jq -se 'last | .replica_error_call_site == "name" and .replica_error_code == "IC0301"' "$log" + # implicit call, wallet canister + wallet=$(dfx identity get-wallet) + dfx canister stop "$wallet" + dfx canister delete "$wallet" --no-withdrawal -y + assert_command_fail dfx canister create e2e_project_backend + assert_command jq -se 'last | .replica_error_call_site == "wallet_api_version" and .replica_error_code == "IC0301"' "$log" + # call to unknown canister + dfx canister create --all --no-wallet + assert_command_fail dfx canister call e2e_project_backend greet + assert_command jq -se 'last | .replica_error_call_site == "" and .replica_error_code == "IC0537"' "$log" + # call to assets canister + install_asset wasm + dfx build e2e_project_frontend + dfx canister install "$(dfx canister id e2e_project_frontend)" --wasm identity/main.wasm + assert_command_fail dfx canister install e2e_project_frontend --mode upgrade --no-asset-upgrade -y + assert_command jq -se 'last | .replica_error_call_site == "list" and .replica_error_code == "IC0536"' "$log" +} + +@test "network information is collected" { + local log + log=$(dfx info telemetry-log-path) + dfx_start + dfx identity get-wallet + assert_command jq -se 'last.network_type == "local-shared"' "$log" + assert_command_fail dfx identity get-wallet --ic + assert_command jq -se 'last.network_type == "ic"' "$log" + setup_actuallylocal_project_network + dfx identity get-wallet --network actuallylocal + assert_command jq -se 'last.network_type == "unknown-configured"' "$log" + setup_ephemeral_project_network + dfx identity get-wallet --network ephemeral + assert_command jq -se 'last.network_type == "project-local"' "$log" + assert_command_fail dfx identity get-wallet --playground + assert_command jq -se 'last.network_type == "playground"' "$log" + assert_command_fail dfx identity get-wallet --network "https://example.com" + assert_command jq -se 'last.network_type == "unknown-url"' "$log" +} + +@test "project structure is collected" { + local log + log=$(dfx info telemetry-log-path) + dfx_start + dfx deploy + assert_command jq -se 'last.project_canisters == [{type: "motoko"}]' "$log" + dfx_new_frontend + dfx deploy + assert_command jq -se 'last.project_canisters | sort_by(.type) == [{type: "assets"}, {type: "motoko"}]' "$log" + install_asset deps/app + dfx deploy || true + assert_command jq -se 'last.project_canisters | sort_by(.type) == [{type: "motoko"}, {type: "pull"}, {type: "pull"}]' "$log" +} + +@test "sender information is collected" { + local log + log=$(dfx info telemetry-log-path) + dfx_start + dfx canister create --all + assert_command jq -se 'last | .cycles_host == "cycles-wallet" and .identity_type == "plaintext"' "$log" + dfx canister delete --all -y + ( + export DFX_CI_MOCK_KEYRING_LOCATION="$MOCK_KEYRING_LOCATION"; + dfx identity new alice + dfx canister create --all --no-wallet --identity alice + ) + assert_command jq -se 'last | .cycles_host == null and .identity_type == "keyring"' "$log" + dfx cycles balance --ic --identity anonymous + assert_command jq -se 'last | .cycles_host == "cycles-ledger" and .identity_type == "anonymous"' "$log" +} diff --git a/e2e/utils/_.bash b/e2e/utils/_.bash index 63e5a462ae..eae1b89a62 100644 --- a/e2e/utils/_.bash +++ b/e2e/utils/_.bash @@ -232,6 +232,10 @@ setup_actuallylocal_project_network() { jq '.networks.actuallylocal.providers=["http://127.0.0.1:'"$webserver_port"'"]' dfx.json | sponge dfx.json } +setup_ephemeral_project_network() { + jq ".networks.ephemeral.bind=\"127.0.0.1:$(get_webserver_port)\"" dfx.json | sponge dfx.json +} + setup_actuallylocal_shared_network() { webserver_port=$(get_webserver_port) [ ! -f "$E2E_NETWORKS_JSON" ] && echo "{}" >"$E2E_NETWORKS_JSON" diff --git a/src/canisters/frontend/ic-asset/src/canister_api/methods/asset_properties.rs b/src/canisters/frontend/ic-asset/src/canister_api/methods/asset_properties.rs index 1c6169a2e3..aebad4ccdf 100644 --- a/src/canisters/frontend/ic-asset/src/canister_api/methods/asset_properties.rs +++ b/src/canisters/frontend/ic-asset/src/canister_api/methods/asset_properties.rs @@ -73,10 +73,12 @@ pub(crate) async fn get_assets_properties( } // older canisters don't have get_assets_properties method // therefore we can break the loop - Err(AgentError::UncertifiedReject(RejectResponse { reject_message, .. })) - if reject_message - .contains(&format!("has no query method '{GET_ASSET_PROPERTIES}'")) - || reject_message.contains("query method does not exist") => + Err(AgentError::UncertifiedReject { + reject: RejectResponse { reject_message, .. }, + .. + }) if reject_message + .contains(&format!("has no query method '{GET_ASSET_PROPERTIES}'")) + || reject_message.contains("query method does not exist") => { break; } diff --git a/src/dfx-core/src/config/model/network_descriptor.rs b/src/dfx-core/src/config/model/network_descriptor.rs index 2565348353..4ebd9f1838 100644 --- a/src/dfx-core/src/config/model/network_descriptor.rs +++ b/src/dfx-core/src/config/model/network_descriptor.rs @@ -33,6 +33,7 @@ pub struct NetworkDescriptor { pub providers: Vec, pub r#type: NetworkTypeDescriptor, pub is_ic: bool, + pub is_ad_hoc: bool, pub local_server_descriptor: Option, } @@ -71,6 +72,7 @@ impl NetworkDescriptor { providers: vec![DEFAULT_IC_GATEWAY.to_string()], r#type: NetworkTypeDescriptor::Persistent, is_ic: true, + is_ad_hoc: false, local_server_descriptor: None, } } @@ -121,6 +123,7 @@ impl NetworkDescriptor { canister_timeout_seconds: MOTOKO_PLAYGROUND_CANISTER_TIMEOUT_SECONDS, }, is_ic: true, + is_ad_hoc: false, local_server_descriptor: None, } } diff --git a/src/dfx-core/src/identity/mod.rs b/src/dfx-core/src/identity/mod.rs index 580353e1ba..5fb9364e29 100644 --- a/src/dfx-core/src/identity/mod.rs +++ b/src/dfx-core/src/identity/mod.rs @@ -77,6 +77,8 @@ pub struct Identity { /// Inner implementation of this identity. inner: Box, + + identity_type: IdentityType, } impl Identity { @@ -85,13 +87,14 @@ impl Identity { name: ANONYMOUS_IDENTITY_NAME.to_string(), inner: Box::new(AnonymousIdentity {}), insecure: false, + identity_type: IdentityType::Anonymous, } } fn basic( name: &str, pem_content: &[u8], - was_encrypted: bool, + identity_type: IdentityType, ) -> Result { let inner = Box::new( BasicIdentity::from_pem(pem_content) @@ -101,14 +104,15 @@ impl Identity { Ok(Self { name: name.to_string(), inner, - insecure: !was_encrypted, + insecure: identity_type == IdentityType::Plaintext, + identity_type, }) } fn secp256k1( name: &str, pem_content: &[u8], - was_encrypted: bool, + identity_type: IdentityType, ) -> Result { let inner = Box::new( Secp256k1Identity::from_pem(pem_content) @@ -118,7 +122,8 @@ impl Identity { Ok(Self { name: name.to_string(), inner, - insecure: !was_encrypted, + insecure: identity_type == IdentityType::Plaintext, + identity_type, }) } @@ -139,6 +144,7 @@ impl Identity { name: name.to_string(), inner, insecure: false, + identity_type: IdentityType::Hsm, }) } @@ -151,11 +157,11 @@ impl Identity { if let Some(hsm) = config.hsm { Identity::hardware(name, hsm).map_err(NewIdentityError::NewHardwareIdentityFailed) } else { - let (pem_content, was_encrypted) = + let (pem_content, identity_type) = pem_safekeeping::load_pem(log, locations, name, &config) .map_err(NewIdentityError::LoadPemFailed)?; - Identity::secp256k1(name, &pem_content, was_encrypted) - .or_else(|e| Identity::basic(name, &pem_content, was_encrypted).map_err(|_| e)) + Identity::secp256k1(name, &pem_content, identity_type) + .or_else(|e| Identity::basic(name, &pem_content, identity_type).map_err(|_| e)) .map_err(NewIdentityError::LoadPemIdentityFailed) } } @@ -166,6 +172,10 @@ impl Identity { &self.name } + pub fn identity_type(&self) -> IdentityType { + self.identity_type + } + /// Logs all wallets that are configured in a WalletGlobalConfig. pub fn display_linked_wallets( logger: &Logger, @@ -304,6 +314,16 @@ impl AsRef for Identity { } } +#[derive(Serialize, Copy, Clone, Debug, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum IdentityType { + Keyring, + Plaintext, + EncryptedLocal, + Hsm, + Anonymous, +} + #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub enum CallSender { SelectedId, diff --git a/src/dfx-core/src/identity/pem_safekeeping.rs b/src/dfx-core/src/identity/pem_safekeeping.rs index bedb8b4d28..09bd64db17 100644 --- a/src/dfx-core/src/identity/pem_safekeeping.rs +++ b/src/dfx-core/src/identity/pem_safekeeping.rs @@ -1,5 +1,5 @@ use super::identity_manager::EncryptionConfiguration; -use super::IdentityConfiguration; +use super::{IdentityConfiguration, IdentityType}; use crate::error::identity::WritePemContentError; use crate::error::{ encryption::{ @@ -32,7 +32,7 @@ pub(crate) fn load_pem( locations: &IdentityFileLocations, identity_name: &str, identity_config: &IdentityConfiguration, -) -> Result<(Vec, bool), LoadPemError> { +) -> Result<(Vec, IdentityType), LoadPemError> { if identity_config.hsm.is_some() { unreachable!("Cannot load pem content for an HSM identity.") } else if identity_config.keyring_identity_suffix.is_some() { @@ -42,7 +42,7 @@ pub(crate) fn load_pem( ); let pem = keyring_mock::load_pem_from_keyring(identity_name) .map_err(|err| LoadFromKeyringFailed(Box::new(identity_name.to_string()), err))?; - Ok((pem, true)) + Ok((pem, IdentityType::Keyring)) } else { let pem_path = locations.get_identity_pem_path(identity_name, identity_config); load_pem_from_file(&pem_path, Some(identity_config)) @@ -83,12 +83,19 @@ pub(crate) fn save_pem( pub fn load_pem_from_file( path: &Path, config: Option<&IdentityConfiguration>, -) -> Result<(Vec, bool), LoadPemFromFileError> { +) -> Result<(Vec, IdentityType), LoadPemFromFileError> { let content = crate::fs::read(path)?; let (content, was_encrypted) = maybe_decrypt_pem(content.as_slice(), config) .map_err(|err| DecryptPemFileFailed(path.to_path_buf(), err))?; - Ok((content, was_encrypted)) + Ok(( + content, + if was_encrypted { + IdentityType::EncryptedLocal + } else { + IdentityType::Plaintext + }, + )) } /// Transparently handles all complexities regarding pem file encryption, including prompting the user for the password. diff --git a/src/dfx-core/src/network/provider.rs b/src/dfx-core/src/network/provider.rs index b19baefe49..0f7ed17f5a 100644 --- a/src/dfx-core/src/network/provider.rs +++ b/src/dfx-core/src/network/provider.rs @@ -82,6 +82,7 @@ fn config_network_to_network_descriptor( playground, )?, is_ic, + is_ad_hoc: false, local_server_descriptor: None, }) } @@ -136,6 +137,7 @@ fn config_network_to_network_descriptor( providers, r#type: network_type, is_ic: false, + is_ad_hoc: false, local_server_descriptor: Some(local_server_descriptor), }) } @@ -212,6 +214,7 @@ fn create_url_based_network_descriptor( providers: vec![url], r#type: network_type, is_ic, + is_ad_hoc: true, local_server_descriptor: None, }) }) diff --git a/src/dfx/Cargo.toml b/src/dfx/Cargo.toml index 8f73145e89..0a3cbfe83f 100644 --- a/src/dfx/Cargo.toml +++ b/src/dfx/Cargo.toml @@ -125,7 +125,7 @@ time = { workspace = true, features = [ ] } tokio = { workspace = true, features = ["full"] } url.workspace = true -uuid = { version = "1.15.1", features = [ "v4", ] } +uuid = { version = "1.15.1", features = ["v4"] } walkdir.workspace = true walrus = "0.21.1" which = "4.2.5" diff --git a/src/dfx/src/commands/canister/call.rs b/src/dfx/src/commands/canister/call.rs index 1e69f2d84f..389e67ec81 100644 --- a/src/dfx/src/commands/canister/call.rs +++ b/src/dfx/src/commands/canister/call.rs @@ -3,6 +3,7 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; use crate::lib::operations::canister::get_canister_id_and_candid_path; use crate::lib::root_key::fetch_root_key_if_needed; +use crate::lib::telemetry::{CyclesHost, Telemetry}; use crate::util::clap::argument_from_cli::ArgumentFromCliPositionalOpt; use crate::util::clap::parsers::cycle_amount_parser; use crate::util::{blob_from_arguments, fetch_remote_did_file, get_candid_type, print_idl_blob}; @@ -335,12 +336,18 @@ To figure out the id of your wallet, run 'dfx identity get-wallet (--network ic) // amount has been validated by cycle_amount_validator let cycles = opts.with_cycles.unwrap_or(0); - if call_sender == &CallSender::SelectedId && cycles != 0 { - let explanation = "It is only possible to send cycles from a canister."; - let action_suggestion = "To send the same function call from your wallet (a canister), run the command using 'dfx canister call (--network ic) --wallet '.\n\ + if cycles != 0 { + match call_sender { + CallSender::SelectedId => { + let explanation = "It is only possible to send cycles from a canister."; + let action_suggestion = "To send the same function call from your wallet (a canister), run the command using 'dfx canister call (--network ic) --wallet '.\n\ To figure out the id of your wallet, run 'dfx identity get-wallet (--network ic)'."; - return Err(DiagnosedError::new(explanation, action_suggestion)) - .context("Function caller is not a canister."); + return Err(DiagnosedError::new(explanation, action_suggestion)) + .context("Function caller is not a canister."); + } + CallSender::Wallet(_) => Telemetry::set_cycles_host(CyclesHost::CyclesWallet), + _ => {} + } } if is_query { diff --git a/src/dfx/src/commands/canister/deposit_cycles.rs b/src/dfx/src/commands/canister/deposit_cycles.rs index 5aba4a9ed5..2425ccf51b 100644 --- a/src/dfx/src/commands/canister/deposit_cycles.rs +++ b/src/dfx/src/commands/canister/deposit_cycles.rs @@ -5,6 +5,7 @@ use crate::lib::identity::wallet::get_or_create_wallet_canister; use crate::lib::operations::canister; use crate::lib::operations::canister::skip_remote_canister; use crate::lib::root_key::fetch_root_key_if_needed; +use crate::lib::telemetry::{CyclesHost, Telemetry}; use crate::lib::{environment::Environment, operations::cycles_ledger}; use crate::util::clap::parsers::{cycle_amount_parser, icrc_subaccount_parser}; use anyhow::{bail, Context}; @@ -57,6 +58,7 @@ async fn deposit_cycles( match call_sender { CallSender::SelectedId => { + Telemetry::set_cycles_host(CyclesHost::CyclesLedger); cycles_ledger::withdraw( env.get_agent(), env.get_logger(), @@ -71,6 +73,7 @@ async fn deposit_cycles( unreachable!("Impersonating sender when depositing cycles is not supported.") } CallSender::Wallet(_) => { + Telemetry::set_cycles_host(CyclesHost::CyclesWallet); canister::deposit_cycles(env, canister_id, call_sender, cycles).await? } }; diff --git a/src/dfx/src/commands/canister/request_status.rs b/src/dfx/src/commands/canister/request_status.rs index bcc81b75e6..22d9f3bd3f 100644 --- a/src/dfx/src/commands/canister/request_status.rs +++ b/src/dfx/src/commands/canister/request_status.rs @@ -57,7 +57,10 @@ pub async fn exec(env: &dyn Environment, opts: RequestStatusOpts) -> DfxResult { match response { RequestStatusResponse::Replied(reply) => return Ok(reply.arg), RequestStatusResponse::Rejected(response) => { - return Err(DfxError::new(AgentError::CertifiedReject(response))) + return Err(DfxError::new(AgentError::CertifiedReject { + reject: response, + operation: None, + })) } RequestStatusResponse::Unknown => (), RequestStatusResponse::Received | RequestStatusResponse::Processing => { diff --git a/src/dfx/src/commands/cycles/mod.rs b/src/dfx/src/commands/cycles/mod.rs index a7768654c5..a2574ad9b5 100644 --- a/src/dfx/src/commands/cycles/mod.rs +++ b/src/dfx/src/commands/cycles/mod.rs @@ -2,6 +2,7 @@ use crate::lib::agent::create_agent_environment; use crate::lib::environment::Environment; use crate::lib::error::DfxResult; use crate::lib::network::network_opt::NetworkOpt; +use crate::lib::telemetry::{CyclesHost, Telemetry}; use clap::Parser; use tokio::runtime::Runtime; @@ -34,6 +35,7 @@ enum SubCommand { } pub fn exec(env: &dyn Environment, opts: CyclesOpts) -> DfxResult { + Telemetry::set_cycles_host(CyclesHost::CyclesLedger); let agent_env = create_agent_environment(env, opts.network.to_network_name())?; let runtime = Runtime::new().expect("Unable to create a runtime"); runtime.block_on(async { diff --git a/src/dfx/src/commands/wallet/mod.rs b/src/dfx/src/commands/wallet/mod.rs index 1686311567..3191c91855 100644 --- a/src/dfx/src/commands/wallet/mod.rs +++ b/src/dfx/src/commands/wallet/mod.rs @@ -4,6 +4,7 @@ use crate::lib::error::DfxResult; use crate::lib::identity::wallet::get_or_create_wallet_canister; use crate::lib::network::network_opt::NetworkOpt; use crate::lib::root_key::fetch_root_key_if_needed; +use crate::lib::telemetry::{CyclesHost, Telemetry}; use anyhow::Context; use candid::utils::ArgumentDecoder; use candid::CandidType; @@ -57,6 +58,7 @@ enum SubCommand { } pub fn exec(env: &dyn Environment, opts: WalletOpts) -> DfxResult { + Telemetry::set_cycles_host(CyclesHost::CyclesWallet); let agent_env = create_agent_environment(env, opts.network.to_network_name())?; let runtime = Runtime::new().expect("Unable to create a runtime"); runtime.block_on(async { diff --git a/src/dfx/src/lib/diagnosis.rs b/src/dfx/src/lib/diagnosis.rs index 344841dee3..2a66b9adeb 100644 --- a/src/dfx/src/lib/diagnosis.rs +++ b/src/dfx/src/lib/diagnosis.rs @@ -112,26 +112,37 @@ fn not_a_controller(err: &AgentError) -> bool { // Newer replicas include the error code in the reject response. matches!( err, - AgentError::UncertifiedReject(RejectResponse { - reject_code: RejectCode::CanisterError, - error_code: Some(error_code), + AgentError::UncertifiedReject { + reject: RejectResponse { + reject_code: RejectCode::CanisterError, + error_code: Some(error_code), + .. + }, .. - }) if error_code == error_code::CANISTER_INVALID_CONTROLLER + } if error_code == error_code::CANISTER_INVALID_CONTROLLER ) } fn wallet_method_not_found(err: &AgentError) -> bool { match err { - AgentError::CertifiedReject(RejectResponse { - reject_code: RejectCode::CanisterError, - reject_message, + AgentError::CertifiedReject { + reject: + RejectResponse { + reject_code: RejectCode::CanisterError, + reject_message, + .. + }, .. - }) if reject_message.contains("Canister has no update method 'wallet_") => true, - AgentError::UncertifiedReject(RejectResponse { - reject_code: RejectCode::CanisterError, - reject_message, + } if reject_message.contains("Canister has no update method 'wallet_") => true, + AgentError::UncertifiedReject { + reject: + RejectResponse { + reject_code: RejectCode::CanisterError, + reject_message, + .. + }, .. - }) if reject_message.contains("Canister has no query method 'wallet_") => true, + } if reject_message.contains("Canister has no query method 'wallet_") => true, _ => false, } } diff --git a/src/dfx/src/lib/environment.rs b/src/dfx/src/lib/environment.rs index 1cadea367a..3fc910408a 100644 --- a/src/dfx/src/lib/environment.rs +++ b/src/dfx/src/lib/environment.rs @@ -2,6 +2,7 @@ use crate::config::cache::VersionCache; use crate::config::dfx_version; use crate::lib::error::DfxResult; use crate::lib::progress_bar::ProgressBar; +use crate::lib::telemetry::{CanisterRecord, Telemetry}; use crate::lib::warning::{is_warning_disabled, DfxWarning::MainnetPlainTextIdentity}; use anyhow::{anyhow, bail}; use candid::Principal; @@ -175,6 +176,14 @@ impl EnvironmentImpl { let config = Config::from_current_dir(Some(&self.extension_manager))?; let project_config = config.map_or(ProjectConfig::NoProject, |config| { + if let Some(canisters) = &config.config.canisters { + Telemetry::set_canisters( + canisters + .values() + .map(CanisterRecord::from_canister) + .collect(), + ); + } ProjectConfig::Loaded(Arc::new(config)) }); self.project_config.replace(project_config); @@ -332,6 +341,8 @@ impl<'a> AgentEnvironment<'a> { } else { identity_manager.instantiate_selected_identity(&logger)? }; + Telemetry::set_identity_type(identity.identity_type()); + Telemetry::set_network(&network_descriptor); if network_descriptor.is_ic && !matches!( network_descriptor.r#type, @@ -417,11 +428,12 @@ impl<'a> Environment for AgentEnvironment<'a> { fn get_canister_id_store(&self) -> Result<&CanisterIdStore, CanisterIdStoreError> { self.canister_id_store.get_or_try_init(|| { - CanisterIdStore::new( - self.get_logger(), - self.get_network_descriptor(), - self.get_config()?, - ) + let config = self.get_config()?; + let network_descriptor = self.get_network_descriptor(); + let store = + CanisterIdStore::new(self.get_logger(), network_descriptor, config.clone())?; + Telemetry::allowlist_all_asset_canisters(config.as_deref(), &store); + Ok(store) }) } diff --git a/src/dfx/src/lib/identity/wallet.rs b/src/dfx/src/lib/identity/wallet.rs index 0b4581f399..71d07b5dcc 100644 --- a/src/dfx/src/lib/identity/wallet.rs +++ b/src/dfx/src/lib/identity/wallet.rs @@ -1,5 +1,6 @@ use crate::lib::error::{DfxError, DfxResult}; use crate::lib::root_key::fetch_root_key_if_needed; +use crate::lib::telemetry::Telemetry; use crate::util::assets::wallet_wasm; use crate::Environment; use anyhow::{bail, Context}; @@ -59,7 +60,10 @@ pub async fn get_or_create_wallet( }) } } - Some(principal) => Ok(principal), + Some(principal) => { + Telemetry::allowlist_canisters(&[principal]); + Ok(principal) + } } } @@ -94,16 +98,22 @@ pub async fn create_wallet( } }; + Telemetry::allowlist_canisters(&[canister_id]); + match mgr .install_code(&canister_id, wasm.as_slice()) .with_mode(InstallMode::Install) .await { - Err(AgentError::CertifiedReject(RejectResponse { - reject_code: RejectCode::CanisterError, - reject_message, + Err(AgentError::CertifiedReject { + reject: + RejectResponse { + reject_code: RejectCode::CanisterError, + reject_message, + .. + }, .. - })) if reject_message.contains("not empty") => { + }) if reject_message.contains("not empty") => { bail!( r#"The wallet canister "{canister_id}" already exists for user "{name}" on "{}" network."#, network.name diff --git a/src/dfx/src/lib/named_canister.rs b/src/dfx/src/lib/named_canister.rs index 68be8e887f..e9d8b3d399 100644 --- a/src/dfx/src/lib/named_canister.rs +++ b/src/dfx/src/lib/named_canister.rs @@ -1,9 +1,9 @@ //! Named canister module. //! //! Contains the Candid UI canister for now -use crate::lib::environment::Environment; use crate::lib::error::DfxResult; use crate::lib::root_key::fetch_root_key_if_needed; +use crate::lib::{environment::Environment, telemetry::Telemetry}; use crate::util; use anyhow::{anyhow, Context}; use candid::Principal; @@ -73,6 +73,7 @@ pub async fn install_ui_canister( .await .context("Install wasm call failed.")?; id_store.add(env.get_logger(), UI_CANISTER, &canister_id.to_text(), None)?; + Telemetry::allowlist_canisters(&[canister_id]); spinner.finish_and_clear(); debug!( env.get_logger(), diff --git a/src/dfx/src/lib/operations/canister/create_canister.rs b/src/dfx/src/lib/operations/canister/create_canister.rs index ee09b1af02..8998fcfa9d 100644 --- a/src/dfx/src/lib/operations/canister/create_canister.rs +++ b/src/dfx/src/lib/operations/canister/create_canister.rs @@ -8,6 +8,7 @@ use crate::lib::identity::wallet::{get_or_create_wallet_canister, GetOrCreateWal use crate::lib::ledger_types::MAINNET_CYCLE_MINTER_CANISTER_ID; use crate::lib::operations::canister::motoko_playground::reserve_canister_with_playground; use crate::lib::operations::cycles_ledger::create_with_cycles_ledger; +use crate::lib::telemetry::{CyclesHost, Telemetry}; use crate::util::clap::subnet_selection_opt::SubnetSelectionType; use anyhow::{anyhow, bail, Context}; use candid::Principal; @@ -190,7 +191,7 @@ The command line value will be used.", canister_id ); canister_id_store.add(log, canister_name, &canister_id, None)?; - + Telemetry::allowlist_all_asset_canisters(env.get_config()?.as_deref(), canister_id_store); Ok(()) } @@ -240,11 +241,15 @@ async fn create_with_management_canister( Err(anyhow!(NEEDS_WALLET)) } } - Err(AgentError::UncertifiedReject(RejectResponse { - reject_code: RejectCode::CanisterReject, - reject_message, + Err(AgentError::UncertifiedReject { + reject: + RejectResponse { + reject_code: RejectCode::CanisterReject, + reject_message, + .. + }, .. - })) if reject_message.contains("is not allowed to call ic00 method") => { + }) if reject_message.contains("is not allowed to call ic00 method") => { Err(anyhow!(NEEDS_WALLET)) } Err(e) => Err(e).context("Canister creation call failed."), @@ -258,6 +263,7 @@ async fn create_with_wallet( settings: DfxCanisterSettings, subnet_selection: &SubnetSelectionType, ) -> DfxResult { + Telemetry::set_cycles_host(CyclesHost::CyclesWallet); let wallet = build_wallet_canister(*wallet_id, agent).await?; let cycles = with_cycles.unwrap_or(CANISTER_CREATE_FEE + CANISTER_INITIAL_CYCLE_BALANCE); diff --git a/src/dfx/src/lib/operations/cycles_ledger.rs b/src/dfx/src/lib/operations/cycles_ledger.rs index 486a81a91f..a36dc007eb 100644 --- a/src/dfx/src/lib/operations/cycles_ledger.rs +++ b/src/dfx/src/lib/operations/cycles_ledger.rs @@ -13,6 +13,7 @@ use crate::lib::operations::canister::create_canister::{ CANISTER_CREATE_FEE, CANISTER_INITIAL_CYCLE_BALANCE, }; use crate::lib::retryable::retryable; +use crate::lib::telemetry::{CyclesHost, Telemetry}; use crate::util::clap::subnet_selection_opt::SubnetSelectionType; use anyhow::{anyhow, bail, Context}; use backoff::future::retry; @@ -316,6 +317,7 @@ pub async fn create_with_cycles_ledger( created_at_time: Option, subnet_selection: &mut SubnetSelectionType, ) -> DfxResult { + Telemetry::set_cycles_host(CyclesHost::CyclesLedger); let cycles = with_cycles.unwrap_or(CANISTER_CREATE_FEE + CANISTER_INITIAL_CYCLE_BALANCE); let resolved_subnet_selection = subnet_selection.resolve(env).await?; let created_at_time = created_at_time.or_else(|| { diff --git a/src/dfx/src/lib/operations/ledger.rs b/src/dfx/src/lib/operations/ledger.rs index cf8866c9b8..7dd34148e8 100644 --- a/src/dfx/src/lib/operations/ledger.rs +++ b/src/dfx/src/lib/operations/ledger.rs @@ -360,11 +360,15 @@ Your principal for ICP wallets and decentralized exchanges: {} fn retryable(agent_error: &AgentError) -> bool { match agent_error { - AgentError::CertifiedReject(RejectResponse { - reject_code: RejectCode::CanisterError, - reject_message, + AgentError::CertifiedReject { + reject: + RejectResponse { + reject_code: RejectCode::CanisterError, + reject_message, + .. + }, .. - }) if reject_message.contains("is out of cycles") => false, + } if reject_message.contains("is out of cycles") => false, AgentError::HttpError(HttpErrorPayload { status, content_type: _, diff --git a/src/dfx/src/lib/telemetry.rs b/src/dfx/src/lib/telemetry.rs index 772b749d35..d60ac376f8 100644 --- a/src/dfx/src/lib/telemetry.rs +++ b/src/dfx/src/lib/telemetry.rs @@ -4,17 +4,28 @@ use crate::config::dfx_version; use crate::lib::error::DfxResult; use crate::CliOpts; use anyhow::Context; +use candid::Principal; use chrono::{Datelike, Local, NaiveDateTime}; use clap::parser::ValueSource; use clap::{ArgMatches, Command, CommandFactory}; use dfx_core::config::directories::project_dirs; -use dfx_core::config::model::dfinity::TelemetryState; +use dfx_core::config::model::canister_id_store::CanisterIdStore; +use dfx_core::config::model::dfinity::{ + CanisterTypeProperties, Config, ConfigCanistersCanister, TelemetryState, +}; +use dfx_core::config::model::local_server_descriptor::LocalNetworkScopeDescriptor; +use dfx_core::config::model::network_descriptor::{NetworkDescriptor, NetworkTypeDescriptor}; use dfx_core::fs; +use dfx_core::identity::IdentityType; use fd_lock::{RwLock as FdRwLock, RwLockWriteGuard}; use fn_error_context::context; +use ic_agent::agent::RejectResponse; +use ic_agent::agent_error::Operation; +use ic_agent::AgentError; use reqwest::StatusCode; use semver::Version; use serde::Serialize; +use std::collections::BTreeSet; use std::ffi::OsString; use std::fs::{File, OpenOptions}; use std::io::Seek; @@ -52,6 +63,13 @@ pub struct Telemetry { arguments: Vec, elapsed: Option, platform: String, + last_reject: Option, + last_operation: Option, + identity_type: Option, + cycles_host: Option, + canisters: Option>, + network_type: Option, + allowlisted_canisters: BTreeSet, week: Option, publish: bool, } @@ -120,6 +138,14 @@ impl Telemetry { }); } + pub fn set_identity_type(identity_type: IdentityType) { + with_telemetry(|telemetry| telemetry.identity_type = Some(identity_type)); + } + + pub fn set_cycles_host(host: CyclesHost) { + with_telemetry(|telemetry| telemetry.cycles_host = Some(host)); + } + pub fn set_week() { with_telemetry(|telemetry| { let iso_week = Local::now().naive_local().iso_week(); @@ -134,6 +160,67 @@ impl Telemetry { }); } + pub fn set_error(error: &anyhow::Error) { + with_telemetry(|telemetry| { + for source in error.chain() { + if let Some(agent_err) = source.downcast_ref::() { + if let AgentError::CertifiedReject { reject, operation } + | AgentError::UncertifiedReject { reject, operation } = agent_err + { + telemetry.last_reject = Some(reject.clone()); + if let Some(operation) = operation { + telemetry.last_operation = Some(operation.clone()); + } + } + break; + } + } + }); + } + + pub fn set_canisters(canisters: Vec) { + with_telemetry(|telemetry| telemetry.canisters = Some(canisters)); + } + + pub fn allowlist_canisters(canisters: &[Principal]) { + with_telemetry(|telemetry| telemetry.allowlisted_canisters.extend(canisters)); + } + + pub fn allowlist_all_asset_canisters(config: Option<&Config>, ids: &CanisterIdStore) { + with_telemetry(|telemetry| { + if let Some(config) = config { + for (name, canister) in config.config.canisters.iter().flatten() { + if let CanisterTypeProperties::Assets { .. } = &canister.type_specific { + if let Ok(canister_id) = ids.get(name) { + telemetry.allowlisted_canisters.insert(canister_id); + } + } + } + } + }) + } + + pub fn set_network(network: &NetworkDescriptor) { + with_telemetry(|telemetry| { + telemetry.network_type = Some( + if let NetworkTypeDescriptor::Playground { .. } = &network.r#type { + NetworkType::Playground + } else if network.is_ic { + NetworkType::Ic + } else if let Some(local) = &network.local_server_descriptor { + match &local.scope { + LocalNetworkScopeDescriptor::Project => NetworkType::ProjectLocal, + LocalNetworkScopeDescriptor::Shared { .. } => NetworkType::LocalShared, + } + } else if network.is_ad_hoc { + NetworkType::UnknownUrl + } else { + NetworkType::UnknownConfigured + }, + ) + }); + } + pub fn append_record(record: &T) -> DfxResult<()> { let record = serde_json::to_string(record)?; let record = record.trim(); @@ -152,6 +239,17 @@ impl Telemetry { pub fn append_current_command_timestamped(exit_code: i32) -> DfxResult<()> { try_with_telemetry(|telemetry| { + let reject = telemetry.last_reject.as_ref(); + let call_site = telemetry.last_operation.as_ref().map(|o| match o { + Operation::Call { method, canister } => { + if telemetry.allowlisted_canisters.contains(canister) { + method + } else { + "" + } + } + Operation::ReadState { .. } | Operation::ReadSubnetState { .. } => "/read_state", + }); let record = CommandRecord { tool: "dfx", version: dfx_version(), @@ -161,13 +259,13 @@ impl Telemetry { week: telemetry.week.as_deref(), exit_code, execution_time_ms: telemetry.elapsed.map(|e| e.as_millis()), - replica_error_call_site: None, - replica_error_code: None, - replica_reject_code: None, - cycles_host: None, - identity_type: None, - network_type: None, - project_canisters: None, + replica_error_call_site: call_site, + replica_error_code: reject.and_then(|r| r.error_code.as_deref()), + replica_reject_code: reject.map(|r| r.reject_code as u8), + cycles_host: telemetry.cycles_host, + identity_type: telemetry.identity_type, + network_type: telemetry.network_type, + project_canisters: telemetry.canisters.as_deref(), }; Self::append_record(&record)?; Ok(()) @@ -464,37 +562,29 @@ struct CommandRecord<'a> { parameters: &'a [Argument], exit_code: i32, execution_time_ms: Option, - replica_error_call_site: Option<&'a str>, //todo - replica_error_code: Option<&'a str>, //todo - replica_reject_code: Option, //todo - cycles_host: Option, //todo - identity_type: Option, //todo - network_type: Option, //todo - project_canisters: Option<&'a [Canister]>, //todo + replica_error_call_site: Option<&'a str>, + replica_error_code: Option<&'a str>, + replica_reject_code: Option, + cycles_host: Option, + identity_type: Option, + network_type: Option, + project_canisters: Option<&'a [CanisterRecord]>, } #[derive(Serialize, Copy, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] -enum CyclesHost { +pub enum CyclesHost { CyclesLedger, CyclesWallet, } #[derive(Serialize, Copy, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] -enum IdentityType { - Keyring, - Plaintext, - EncryptedLocal, - Hsm, -} - -#[derive(Serialize, Copy, Clone, Debug, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -enum NetworkType { +pub enum NetworkType { LocalShared, ProjectLocal, Ic, + Playground, UnknownConfigured, UnknownUrl, } @@ -509,11 +599,24 @@ enum CanisterType { Pull, } -#[derive(Serialize, Copy, Clone, Debug)] -struct Canister { +#[derive(Serialize, Copy, Clone, Debug, PartialEq, Eq)] +pub struct CanisterRecord { r#type: CanisterType, } +impl CanisterRecord { + pub fn from_canister(config: &ConfigCanistersCanister) -> Self { + let r#type = match &config.type_specific { + CanisterTypeProperties::Rust { .. } => CanisterType::Rust, + CanisterTypeProperties::Assets { .. } => CanisterType::Assets, + CanisterTypeProperties::Motoko { .. } => CanisterType::Motoko, + CanisterTypeProperties::Custom { .. } => CanisterType::Custom, + CanisterTypeProperties::Pull { .. } => CanisterType::Pull, + }; + Self { r#type } + } +} + /// Finds the deepest subcommand in both `ArgMatches` and `Command`. fn get_deepest_subcommand<'a>( matches: &'a ArgMatches, diff --git a/src/dfx/src/main.rs b/src/dfx/src/main.rs index 7bff257134..1437fbede4 100644 --- a/src/dfx/src/main.rs +++ b/src/dfx/src/main.rs @@ -17,6 +17,7 @@ use std::collections::HashMap; use std::ffi::OsString; use std::path::PathBuf; use std::time::Instant; +use util::default_allowlisted_canisters; mod actors; mod commands; @@ -149,6 +150,7 @@ fn get_args_altered_for_extension_run( fn inner_main(log_level: &mut Option) -> DfxResult { let tool_config = ToolConfig::new()?; Telemetry::init(tool_config.interface().telemetry); + Telemetry::allowlist_canisters(default_allowlisted_canisters()); let em = ExtensionManager::new(dfx_version())?; let installed_extension_manifests = em.load_installed_extension_manifests()?; @@ -197,6 +199,7 @@ fn main() { let result = inner_main(&mut log_level); let exit_code = if let Err(err) = result { + Telemetry::set_error(&err); let error_diagnosis = diagnose(&err); print_error_and_diagnosis(log_level, err, error_diagnosis); 255 diff --git a/src/dfx/src/util/mod.rs b/src/dfx/src/util/mod.rs index 2c08c25434..0afbab1ef1 100644 --- a/src/dfx/src/util/mod.rs +++ b/src/dfx/src/util/mod.rs @@ -1,5 +1,8 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; +use crate::lib::integrations::bitcoin::MAINNET_BITCOIN_CANISTER_ID; +use crate::lib::ledger_types::{MAINNET_CYCLE_MINTER_CANISTER_ID, MAINNET_LEDGER_CANISTER_ID}; +use crate::lib::subnet::MAINNET_REGISTRY_CANISTER_ID; use crate::{error_invalid_argument, error_invalid_data, error_unknown}; use anyhow::{anyhow, bail, Context}; use backoff::backoff::Backoff; @@ -9,6 +12,7 @@ use candid::types::{value::IDLValue, Function, Type, TypeEnv, TypeInner}; use candid::{Decode, Encode, IDLArgs, Principal}; use candid_parser::error::pretty_wrap; use candid_parser::utils::CandidSource; +use dfx_core::config::model::network_descriptor::MAINNET_MOTOKO_PLAYGROUND_CANISTER_ID; use dfx_core::error::cli::UserConsent; use dfx_core::fs::create_dir_all; use fn_error_context::context; @@ -444,6 +448,24 @@ pub fn ask_for_consent(env: &dyn Environment, message: &str) -> Result<(), UserC }) } +pub fn default_allowlisted_canisters() -> &'static [Principal] { + const MAINNET_GOVERNANCE_CANISTER_ID: Principal = + Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01]); + const MAINNET_II_CANISTER_ID: Principal = + Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x01, 0x01]); + &[ + MAINNET_LEDGER_CANISTER_ID, + MAINNET_REGISTRY_CANISTER_ID, + MAINNET_MOTOKO_PLAYGROUND_CANISTER_ID, + MAINNET_REGISTRY_CANISTER_ID, + MAINNET_CYCLE_MINTER_CANISTER_ID, + MAINNET_BITCOIN_CANISTER_ID, + MAINNET_GOVERNANCE_CANISTER_ID, + MAINNET_II_CANISTER_ID, + const { Principal::management_canister() }, + ] +} + pub fn with_suspend_all_spinners(env: &dyn Environment, f: impl FnOnce() -> R) -> R { let mut r = None; env.with_suspend_all_spinners(Box::new(|| r = Some(f())));