diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 72d367b0..87dc5875 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,6 +24,11 @@ jobs: - name: Run cargo check run: cargo check + - name: Run cargo clippy + uses: actions-rs/cargo@v1 + with: + command: clippy -- -D warnings + test: name: Test -- ${{ matrix.os }} -- ${{ matrix.rust_version }} runs-on: ${{ matrix.os }} diff --git a/crates/cargo-lambda-build/src/lib.rs b/crates/cargo-lambda-build/src/lib.rs index 03c20c16..ed52d5cf 100644 --- a/crates/cargo-lambda-build/src/lib.rs +++ b/crates/cargo-lambda-build/src/lib.rs @@ -145,6 +145,7 @@ impl Build { } } +<<<<<<< HEAD let mut build_config = function_build_metadata(&metadata)?; if self.compiler == CompilerFlag::Cargo && build_config.is_zig_enabled() { build_config.compiler = CompilerOptions::from(self.compiler.to_string()); @@ -157,6 +158,9 @@ impl Build { return Err(BuildError::InvalidLinkerOption.into()); } } +======= + let mut cmd = self.build.build_command().map_err(|e| miette::miette!(e))?; +>>>>>>> Add telemetry to track usage. let rust_flags = if self.build.release { let mut rust_flags = env::var("RUSTFLAGS").unwrap_or_default(); diff --git a/crates/cargo-lambda-cli/Cargo.toml b/crates/cargo-lambda-cli/Cargo.toml index c10a18cc..e148e762 100644 --- a/crates/cargo-lambda-cli/Cargo.toml +++ b/crates/cargo-lambda-cli/Cargo.toml @@ -24,9 +24,20 @@ cargo-lambda-new.workspace = true cargo-lambda-watch.workspace = true clap = { workspace = true, features = ["suggestions"] } miette = { workspace = true, features = ["fancy"] } +reqwest = { workspace = true, features = ["rustls-tls", "json"] } +sentry = { version = "0.27.0", default-features = false, features = [ + "rustls", + "reqwest", + "tracing", +] } +sentry-core = "0.27.0" +serde = { version = "1.0.137", features = ["derive"] } +serde_json = "1.0.81" +sysinfo = "0.26.3" tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } tracing.workspace = true tracing-subscriber.workspace = true +uuid.workspace = true [build-dependencies] build-data = "0.1" diff --git a/crates/cargo-lambda-cli/src/error_reporting.rs b/crates/cargo-lambda-cli/src/error_reporting.rs new file mode 100644 index 00000000..633d820d --- /dev/null +++ b/crates/cargo-lambda-cli/src/error_reporting.rs @@ -0,0 +1,23 @@ +use sentry_core::{types::Uuid, Hub}; + +use crate::telemetry::Data; + +pub const SENTRY_DSN: &str = ""; + +pub fn capture_error(err: &miette::Error) -> Uuid { + Hub::with_active(|hub| hub.capture_miette(err)) +} + +/// Hub extension methods for working with [`miette`]. +pub trait MietteHubExt { + /// Captures an [`miette::Error`] on a specific hub. + fn capture_miette(&self, e: &miette::Error) -> Uuid; +} + +impl MietteHubExt for Hub { + fn capture_miette(&self, err: &miette::Error) -> Uuid { + sentry_core::add_breadcrumb(Data::breadcrumb()); + let event = sentry_core::event_from_error(err.root_cause()); + self.capture_event(event) + } +} diff --git a/crates/cargo-lambda-cli/src/main.rs b/crates/cargo-lambda-cli/src/main.rs index 4e3f9b7b..95d24c36 100644 --- a/crates/cargo-lambda-cli/src/main.rs +++ b/crates/cargo-lambda-cli/src/main.rs @@ -1,20 +1,26 @@ #![warn(rust_2018_idioms, unused_lifetimes, clippy::multiple_crate_versions)] use std::boxed::Box; -use cargo_lambda_build::{Build, Zig}; +use cargo_lambda_build::{Build, Zig as ZigBuild}; use cargo_lambda_deploy::Deploy; -use cargo_lambda_invoke::Invoke; +use cargo_lambda_invoke::{is_remote_invoke_err, Invoke}; use cargo_lambda_new::New; use cargo_lambda_watch::Watch; use clap::{CommandFactory, Parser, Subcommand}; +use error_reporting::capture_error; use miette::{miette, IntoDiagnostic, Result}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +mod error_reporting; +use error_reporting::*; +mod telemetry; +use telemetry::*; + #[derive(Parser)] #[command(name = "cargo", bin_name = "cargo", disable_version_flag = true)] enum App { Lambda(Lambda), - #[command(subcommand, hide = true)] + #[command(hide = true)] Zig(Zig), } @@ -29,6 +35,9 @@ struct Lambda { /// Print version information #[arg(short = 'V', long)] version: bool, + /// Disable telemetry and error reporting + #[clap(long, env = "DO_NOT_TRACK")] + do_not_track: bool, } #[derive(Clone, Debug, Subcommand)] @@ -45,6 +54,72 @@ enum LambdaSubcommand { Watch(Watch), } +#[derive(Clone, Debug, Parser)] +struct Zig { + #[clap(subcommand)] + subcommand: ZigBuild, + + /// Disable telemetry and error reporting + #[clap(long, env = "DO_NOT_TRACK")] + do_not_track: bool, +} + +impl Lambda { + async fn run(self) -> Result<()> { + if self.do_not_track && !is_do_not_track_enabled() { + enable_do_not_track(); + } + + let tm_handle = send_telemetry_data(); + + if self.version { + return print_version(); + } + + let subcommand = match self.subcommand { + None => return print_help(), + Some(subcommand) => subcommand, + }; + + let log_directive = if self.verbose == 0 { + std::env::var("RUST_LOG").unwrap_or_else(|_| "cargo_lambda=info".into()) + } else if self.verbose == 1 { + "cargo_lambda=debug".into() + } else { + "cargo_lambda=trace".into() + }; + + let fmt = tracing_subscriber::fmt::layer() + .with_target(false) + .without_time(); + + let subscriber = tracing_subscriber::registry() + .with(sentry::integrations::tracing::layer()) + .with(tracing_subscriber::EnvFilter::new(log_directive)) + .with(fmt); + + if let LambdaSubcommand::Watch(w) = &*subcommand { + subscriber.with(w.xray_layer()).init(); + } else { + subscriber.init(); + } + + let res = subcommand.run().await; + let _ = tm_handle.await; + + res + } +} + +impl Zig { + fn run(self) -> Result<()> { + if self.do_not_track && !is_do_not_track_enabled() { + enable_do_not_track(); + } + self.subcommand.execute().map_err(|e| miette!(e)) + } +} + impl LambdaSubcommand { async fn run(self) -> Result<()> { match self { @@ -58,11 +133,7 @@ impl LambdaSubcommand { } fn print_version() -> Result<()> { - println!( - "cargo-lambda {} {}", - env!("CARGO_PKG_VERSION"), - env!("CARGO_LAMBDA_BUILD_INFO") - ); + println!("cargo-lambda {}", version()); Ok(()) } @@ -82,45 +153,33 @@ fn print_help() -> Result<()> { } } -#[tokio::main] -async fn main() -> Result<()> { +async fn run_command() -> Result<()> { let app = App::parse(); - let lambda = match app { - App::Zig(zig) => return zig.execute().map_err(|e| miette!(e)), - App::Lambda(lambda) => lambda, - }; - - if lambda.version { - return print_version(); + match app { + App::Zig(zig) => zig.run(), + App::Lambda(lambda) => lambda.run().await, } +} - let subcommand = match lambda.subcommand { - None => return print_help(), - Some(subcommand) => subcommand, - }; - - let log_directive = if lambda.verbose == 0 { - std::env::var("RUST_LOG").unwrap_or_else(|_| "cargo_lambda=info".into()) - } else if lambda.verbose == 1 { - "cargo_lambda=debug".into() - } else { - "cargo_lambda=trace".into() - }; - - let fmt = tracing_subscriber::fmt::layer() - .with_target(false) - .without_time(); - - let subscriber = tracing_subscriber::registry() - .with(tracing_subscriber::EnvFilter::new(log_directive)) - .with(fmt); - - if let LambdaSubcommand::Watch(w) = &*subcommand { - subscriber.with(w.xray_layer()).init(); - } else { - subscriber.init(); +#[tokio::main] +async fn main() -> Result<()> { + let _sentry = sentry::init(( + SENTRY_DSN, + sentry::ClientOptions { + release: Some(version().into()), + traces_sample_rate: 1.0, + ..Default::default() + }, + )); + + let res = run_command().await; + + if let Err(err) = res.as_ref() { + if !is_do_not_track_enabled() && !is_remote_invoke_err(err) { + capture_error(err); + } } - subcommand.run().await + res } diff --git a/crates/cargo-lambda-cli/src/telemetry.rs b/crates/cargo-lambda-cli/src/telemetry.rs new file mode 100644 index 00000000..e9a257d2 --- /dev/null +++ b/crates/cargo-lambda-cli/src/telemetry.rs @@ -0,0 +1,105 @@ +use miette::{IntoDiagnostic, Result}; +use sentry::Breadcrumb; +use serde::Serialize; +use std::{ + collections::{BTreeMap, HashMap}, + env, +}; +use sysinfo::{System, SystemExt}; +use tokio::task::JoinHandle; + +const TELEMETRY_URL: &str = ""; + +#[derive(Debug, Serialize)] +pub(crate) struct Data<'a> { + device_id: uuid::Uuid, + os_name: Option, + os_version: Option, + app_version: String, + event_properties: HashMap<&'a str, String>, +} + +impl<'a> Default for Data<'a> { + fn default() -> Self { + let system = System::default(); + let mut args = env::args_os(); + let _ = args.next(); // program name + let _ = args.next(); // `lambda` as subcommand + let args = args + .map(|a| a.to_string_lossy().to_string()) + .collect::>() + .join(" "); + + let mut event_properties = HashMap::new(); + event_properties.insert("arguments", args); + Data { + device_id: uuid::Uuid::new_v4(), + os_name: system.name(), + os_version: system.os_version(), + app_version: version(), + event_properties, + } + } +} + +impl<'a> Data<'a> { + pub(crate) fn breadcrumb() -> Breadcrumb { + let tm = Data::default(); + let mut data = BTreeMap::new(); + if let Some(name) = tm.os_name { + data.insert("os_name".to_string(), name.into()); + } + if let Some(version) = tm.os_version { + data.insert("os_version".to_string(), version.into()); + } + data.insert( + "arguments".to_string(), + tm.event_properties["arguments"].clone().into(), + ); + Breadcrumb { + data, + ..Default::default() + } + } +} + +pub(crate) fn version() -> String { + format!( + "{} {}", + env!("CARGO_PKG_VERSION"), + env!("CARGO_LAMBDA_BUILD_INFO") + ) + .trim_end() + .into() +} + +pub(crate) async fn send_telemetry_data() -> JoinHandle> { + if is_do_not_track_enabled() { + tokio::spawn(async { Ok(()) }) + } else { + tokio::spawn(async { send_environment_data().await }) + } +} + +pub(crate) fn is_do_not_track_enabled() -> bool { + env::var("DO_NOT_TRACK").is_ok() +} + +pub(crate) fn enable_do_not_track() { + env::set_var("DO_NOT_TRACK", "true") +} + +async fn send_environment_data() -> Result<()> { + let data = Data::default(); + + let client = reqwest::Client::new(); + let res = client + .post(TELEMETRY_URL) + .json(&data) + .send() + .await + .into_diagnostic()?; + + tracing::debug!(status = %res.status(), data = ?data, "telemetry data sent"); + Ok(()) +} diff --git a/crates/cargo-lambda-invoke/src/lib.rs b/crates/cargo-lambda-invoke/src/lib.rs index 16880f29..e74865bd 100644 --- a/crates/cargo-lambda-invoke/src/lib.rs +++ b/crates/cargo-lambda-invoke/src/lib.rs @@ -3,7 +3,7 @@ use cargo_lambda_remote::{ RemoteConfig, }; use clap::{Args, ValueHint}; -use miette::{IntoDiagnostic, Result, WrapErr}; +use miette::{IntoDiagnostic, Report, Result, WrapErr}; use reqwest::{Client, StatusCode}; use serde_json::{from_str, to_string_pretty, value::Value}; use std::{ @@ -123,6 +123,7 @@ impl Invoke { Ok(()) } + #[tracing::instrument(skip(self, data), target = "cargo_lambda")] async fn invoke_remote(&self, data: &str) -> Result { if self.function_name == DEFAULT_PACKAGE_FUNCTION { return Err(InvokeError::InvalidFunctionName.into()); @@ -157,6 +158,7 @@ impl Invoke { } } + #[tracing::instrument(skip(self, data), target = "cargo_lambda")] async fn invoke_local(&self, data: &str) -> Result { let host = parse_invoke_ip_address(&self.invoke_address)?; @@ -190,6 +192,7 @@ impl Invoke { } } +#[tracing::instrument(skip(cache), target = "cargo_lambda")] async fn download_example(name: &str, cache: Option) -> Result { let target = format!("https://github.com/LegNeato/aws-lambda-events/raw/master/aws_lambda_events/src/generated/fixtures/{name}"); @@ -227,6 +230,10 @@ fn parse_invoke_ip_address(address: &str) -> Result { Ok(invoke_address) } +pub fn is_remote_invoke_err(err: &Report) -> bool { + err.downcast_ref::().is_some() +} + #[cfg(test)] mod test { use super::*;