From 153969955d758212f68533b70bde2a2be6e74db9 Mon Sep 17 00:00:00 2001 From: Elias Ram Date: Tue, 14 May 2024 16:12:44 +0200 Subject: [PATCH] feat(commands): add send-metric command Add CLI command that can emit metrics. Fixes GH-2001 --- Cargo.lock | 114 +++++++++--- Cargo.toml | 6 +- src/api/envelopes_api.rs | 45 +++++ src/api/errors/api_error.rs | 6 + src/api/mod.rs | 7 + src/commands/derive_parser.rs | 36 ++++ src/commands/mod.rs | 3 + src/commands/send_metric/common_args.rs | 66 +++++++ src/commands/send_metric/distribution.rs | 16 ++ src/commands/send_metric/gauge.rs | 16 ++ src/commands/send_metric/increment.rs | 31 ++++ src/commands/send_metric/mod.rs | 57 ++++++ src/commands/send_metric/set.rs | 44 +++++ src/utils/metrics/mod.rs | 7 + src/utils/metrics/normalized_name.rs | 43 +++++ src/utils/metrics/normalized_payload.rs | 76 ++++++++ src/utils/metrics/normalized_tags.rs | 171 ++++++++++++++++++ src/utils/metrics/normalized_unit.rs | 65 +++++++ src/utils/metrics/types.rs | 20 ++ src/utils/metrics/values.rs | 16 ++ src/utils/mod.rs | 2 + src/utils/value_parsers.rs | 12 ++ .../_cases/help/help-windows.trycmd | 1 + tests/integration/_cases/help/help.trycmd | 1 + ...n-all-options-long-with-float-value.trycmd | 10 + ...on-all-options-short-with-int-value.trycmd | 10 + .../send_metric-distribution-help.trycmd | 26 +++ ...send_metric-distribution-no-options.trycmd | 12 ++ ...d_metric-distribution-normalization.trycmd | 10 + ...c-distribution-numerical-key-prefix.trycmd | 8 + ...equired-options-with-negative-value.trycmd | 10 + ...e-all-options-long-with-float-value.trycmd | 10 + ...ge-all-options-short-with-int-value.trycmd | 10 + .../send_metric/send_metric-gauge-help.trycmd | 26 +++ .../send_metric-gauge-no-options.trycmd | 12 ++ .../send_metric-gauge-normalization.trycmd | 10 + ...d_metric-gauge-numerical-key-prefix.trycmd | 8 + ...equired-options-with-negative-value.trycmd | 10 + .../send_metric-global-options.trycmd | 14 ++ .../send_metric/send_metric-help.trycmd | 46 +++++ ...t-all-options-long-with-float-value.trycmd | 10 + ...nt-all-options-short-with-int-value.trycmd | 10 + .../send_metric-increment-help.trycmd | 26 +++ .../send_metric-increment-no-dsn.trycmd | 10 + .../send_metric-increment-no-options.trycmd | 11 ++ ...t-normalization-with-negative-value.trycmd | 10 + ...tric-increment-numerical-key-prefix.trycmd | 8 + ...d_metric-increment-required-options.trycmd | 10 + .../send_metric-increment-tag-no-colon.trycmd | 8 + ...ric-increment-unsuccessful-api-call.trycmd | 10 + .../send_metric-no-subcommand.trycmd | 26 +++ ...-options-long-with-alphabetic-value.trycmd | 10 + ...et-all-options-short-with-int-value.trycmd | 10 + .../send_metric-set-float-value.trycmd | 10 + .../send_metric/send_metric-set-help.trycmd | 27 +++ .../send_metric-set-no-options.trycmd | 12 ++ .../send_metric-set-normalization.trycmd | 10 + ...end_metric-set-numerical-key-prefix.trycmd | 8 + ...equired-options-with-negative-value.trycmd | 10 + tests/integration/mod.rs | 19 +- tests/integration/send_metric/distribution.rs | 49 +++++ tests/integration/send_metric/gauge.rs | 49 +++++ tests/integration/send_metric/increment.rs | 69 +++++++ tests/integration/send_metric/mod.rs | 34 ++++ tests/integration/send_metric/set.rs | 50 +++++ 65 files changed, 1572 insertions(+), 27 deletions(-) create mode 100644 src/api/envelopes_api.rs create mode 100644 src/commands/derive_parser.rs create mode 100644 src/commands/send_metric/common_args.rs create mode 100644 src/commands/send_metric/distribution.rs create mode 100644 src/commands/send_metric/gauge.rs create mode 100644 src/commands/send_metric/increment.rs create mode 100644 src/commands/send_metric/mod.rs create mode 100644 src/commands/send_metric/set.rs create mode 100644 src/utils/metrics/mod.rs create mode 100644 src/utils/metrics/normalized_name.rs create mode 100644 src/utils/metrics/normalized_payload.rs create mode 100644 src/utils/metrics/normalized_tags.rs create mode 100644 src/utils/metrics/normalized_unit.rs create mode 100644 src/utils/metrics/types.rs create mode 100644 src/utils/metrics/values.rs create mode 100644 src/utils/value_parsers.rs create mode 100644 tests/integration/_cases/send_metric/send_metric-distribution-all-options-long-with-float-value.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-distribution-all-options-short-with-int-value.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-distribution-help.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-distribution-no-options.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-distribution-normalization.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-distribution-numerical-key-prefix.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-distribution-required-options-with-negative-value.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-gauge-all-options-long-with-float-value.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-gauge-all-options-short-with-int-value.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-gauge-help.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-gauge-no-options.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-gauge-normalization.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-gauge-numerical-key-prefix.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-gauge-required-options-with-negative-value.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-global-options.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-help.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-increment-all-options-long-with-float-value.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-increment-all-options-short-with-int-value.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-increment-help.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-increment-no-dsn.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-increment-no-options.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-increment-normalization-with-negative-value.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-increment-numerical-key-prefix.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-increment-required-options.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-increment-tag-no-colon.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-increment-unsuccessful-api-call.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-no-subcommand.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-set-all-options-long-with-alphabetic-value.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-set-all-options-short-with-int-value.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-set-float-value.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-set-help.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-set-no-options.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-set-normalization.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-set-numerical-key-prefix.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-set-required-options-with-negative-value.trycmd create mode 100644 tests/integration/send_metric/distribution.rs create mode 100644 tests/integration/send_metric/gauge.rs create mode 100644 tests/integration/send_metric/increment.rs create mode 100644 tests/integration/send_metric/mod.rs create mode 100644 tests/integration/send_metric/set.rs diff --git a/Cargo.lock b/Cargo.lock index c7b26a6574..58270adf85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -356,7 +356,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0b0588d44d4d63a87dbd75c136c166bbfd9a86a31cb89e09906521c7d3f5e3" dependencies = [ "bitflags 1.3.2", + "clap_derive", "clap_lex", + "once_cell", "strsim", "terminal_size", ] @@ -370,6 +372,19 @@ dependencies = [ "clap", ] +[[package]] +name = "clap_derive" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.107", +] + [[package]] name = "clap_lex" version = "0.3.1" @@ -462,9 +477,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" dependencies = [ "cfg-if", ] @@ -1086,6 +1101,12 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1121,13 +1142,13 @@ dependencies = [ [[package]] name = "hostname" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" dependencies = [ + "cfg-if", "libc", - "match_cfg", - "winapi 0.3.9", + "windows", ] [[package]] @@ -1484,12 +1505,6 @@ dependencies = [ "vlq", ] -[[package]] -name = "match_cfg" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" - [[package]] name = "maybe-owned" version = "0.3.4" @@ -2007,6 +2022,30 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.107", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.66" @@ -2396,9 +2435,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "sentry" -version = "0.32.2" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "766448f12e44d68e675d5789a261515c46ac6ccd240abdd451a9c46c84a49523" +checksum = "00421ed8fa0c995f07cde48ba6c89e80f2b312f74ff637326f392fbfd23abe02" dependencies = [ "curl", "httpdate", @@ -2409,9 +2448,9 @@ dependencies = [ [[package]] name = "sentry-anyhow" -version = "0.32.2" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4da4015667c99f88d68ca7ff02b90c762d6154a4ceb7c02922b9a1dbd3959eeb" +checksum = "ddcbce6e6785c2d91e67c55196f60ac561fab5946b6c7d60cc29f498fc126076" dependencies = [ "anyhow", "sentry-backtrace", @@ -2420,9 +2459,9 @@ dependencies = [ [[package]] name = "sentry-backtrace" -version = "0.32.2" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32701cad8b3c78101e1cd33039303154791b0ff22e7802ed8cc23212ef478b45" +checksum = "a79194074f34b0cbe5dd33896e5928bbc6ab63a889bd9df2264af5acb186921e" dependencies = [ "backtrace", "once_cell", @@ -2446,6 +2485,7 @@ dependencies = [ "clap", "clap_complete", "console", + "crc32fast", "crossbeam-channel", "curl", "data-encoding", @@ -2496,6 +2536,7 @@ dependencies = [ "testing_logger", "thiserror", "trycmd", + "unicode-segmentation", "unix-daemonize", "url", "username", @@ -2508,9 +2549,9 @@ dependencies = [ [[package]] name = "sentry-contexts" -version = "0.32.2" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ddd2a91a13805bd8dab4ebf47323426f758c35f7bf24eacc1aded9668f3824" +checksum = "eba8870c5dba2bfd9db25c75574a11429f6b95957b0a78ac02e2970dd7a5249a" dependencies = [ "hostname", "libc", @@ -2522,9 +2563,9 @@ dependencies = [ [[package]] name = "sentry-core" -version = "0.32.2" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1189f68d7e7e102ef7171adf75f83a59607fafd1a5eecc9dc06c026ff3bdec4" +checksum = "46a75011ea1c0d5c46e9e57df03ce81f5c7f0a9e199086334a1f9c0a541e0826" dependencies = [ "once_cell", "rand", @@ -2535,9 +2576,9 @@ dependencies = [ [[package]] name = "sentry-types" -version = "0.32.2" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7173fd594569091f68a7c37a886e202f4d0c1db1e1fa1d18a051ba695b2e2ec" +checksum = "4519c900ce734f7a0eb7aba0869dfb225a7af8820634a7dd51449e3b093cfb7c" dependencies = [ "debugid", "hex", @@ -3135,6 +3176,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + [[package]] name = "unicode-width" version = "0.1.10" @@ -3355,6 +3402,25 @@ 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.0", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-sys" version = "0.42.0" diff --git a/Cargo.toml b/Cargo.toml index b3bd90408b..797614c9dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ bytecount = "0.6.3" chardet = "0.2.4" chrono = { version = "0.4.31", features = ["serde"] } clap = { version = "4.1.6", default-features = false, features = [ + "derive", "std", "suggestions", "wrap_help", @@ -26,6 +27,7 @@ clap = { version = "4.1.6", default-features = false, features = [ ] } clap_complete = "4.4.3" console = "0.15.5" +crc32fast = "1.4.0" curl = { version = "0.4.44", features = ["static-curl", "static-ssl"] } dirs = "4.0.0" dotenv = "0.15.0" @@ -58,10 +60,11 @@ regex = "1.7.3" runas = "1.0.0" rust-ini = "0.18.0" semver = "1.0.16" -sentry = { version = "0.32.2", default-features = false, features = [ +sentry = { version = "0.32.3", default-features = false, features = [ "anyhow", "curl", "contexts", + "UNSTABLE_metrics", ] } serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.93" @@ -69,6 +72,7 @@ sha1_smol = { version = "1.0.0", features = ["serde"] } sourcemap = { version = "7.0.1", features = ["ram_bundle"] } symbolic = { version = "12.4.1", features = ["debuginfo-serde", "il2cpp"] } thiserror = "1.0.38" +unicode-segmentation = "1.11.0" url = "2.3.1" username = "0.2.0" uuid = { version = "1.3.0", features = ["v4", "serde"] } diff --git a/src/api/envelopes_api.rs b/src/api/envelopes_api.rs new file mode 100644 index 0000000000..9c0e3b88ab --- /dev/null +++ b/src/api/envelopes_api.rs @@ -0,0 +1,45 @@ +use super::{ + errors::{ApiErrorKind, ApiResult}, + Api, ApiResponse, Method, +}; +use crate::{api::errors::ApiError, constants::USER_AGENT}; +use log::debug; +use sentry::{protocol::EnvelopeItem, types::Dsn, Envelope}; +use std::sync::Arc; + +pub struct EnvelopesApi { + api: Arc, + dsn: Dsn, +} + +impl EnvelopesApi { + pub fn try_new() -> ApiResult { + let api = Api::current(); + api.config + .get_dsn() + .map(|dsn| EnvelopesApi { api, dsn }) + .map_err(|_| ApiErrorKind::DsnMissing.into()) + } + + pub fn send_item(&self, item: EnvelopeItem) -> ApiResult { + let mut envelope = Envelope::new(); + envelope.add_item(item); + self.send_envelope(envelope) + } + + pub fn send_envelope(&self, envelope: Envelope) -> ApiResult { + let mut body = vec![]; + envelope + .to_writer(&mut body) + .map_err(|e| ApiError::with_source(ApiErrorKind::CannotSerializeEnvelope, e))?; + debug!("Sending envelope:\n{}", String::from_utf8_lossy(&body)); + let url = self.dsn.envelope_api_url(); + let auth = self.dsn.to_auth(Some(USER_AGENT)); + self.api + .request(Method::Post, url.as_str(), None)? + .with_header("X-Sentry-Auth", &auth.to_string())? + .with_body(body)? + .send()? + .into_result() + } +} diff --git a/src/api/errors/api_error.rs b/src/api/errors/api_error.rs index f23fc309ee..c32b9df51b 100644 --- a/src/api/errors/api_error.rs +++ b/src/api/errors/api_error.rs @@ -12,6 +12,8 @@ pub struct ApiError { pub(in crate::api) enum ApiErrorKind { #[error("could not serialize value as JSON")] CannotSerializeAsJson, + #[error("could not serialize envelope")] + CannotSerializeEnvelope, #[error("could not parse JSON response")] BadJson, #[error("not a JSON response")] @@ -38,6 +40,10 @@ pub(in crate::api) enum ApiErrorKind { "Auth token is required for this request. Please run `sentry-cli login` and try again!" )] AuthMissing, + #[error( + "DSN missing. Please set the `SENTRY_DSN` environment variable to your project's DSN." + )] + DsnMissing, } impl fmt::Display for ApiError { diff --git a/src/api/mod.rs b/src/api/mod.rs index e67366c7e2..45b0b0e131 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -3,6 +3,8 @@ //! to the GitHub API to figure out if there are new releases of the //! sentry-cli tool. +pub mod envelopes_api; + mod connection_manager; mod encoding; mod errors; @@ -1746,6 +1748,11 @@ impl ApiRequest { Ok(self) } + pub fn with_body(mut self, body: Vec) -> ApiResult { + self.body = Some(body); + Ok(self) + } + /// attaches some form data to the request. pub fn with_form_data(mut self, form: curl::easy::Form) -> ApiResult { debug!("sending form data"); diff --git a/src/commands/derive_parser.rs b/src/commands/derive_parser.rs new file mode 100644 index 0000000000..219fc3052e --- /dev/null +++ b/src/commands/derive_parser.rs @@ -0,0 +1,36 @@ +use crate::utils::auth_token::AuthToken; +use crate::utils::value_parsers::kv_parser; +use clap::{command, value_parser, ArgAction::SetTrue, Parser, Subcommand}; + +use super::send_metric::SendMetricArgs; + +#[derive(Parser)] +pub(super) struct SentryCLI { + #[command(subcommand)] + pub(super) command: SentryCLICommand, + + #[arg(global=true, long="header", value_name="KEY:VALUE", value_parser=kv_parser)] + #[arg(help = "Custom headers that should be attached to all requests{n}in key:value format")] + pub(super) headers: Vec<(String, String)>, + + #[arg(global=true, long, value_parser=value_parser!(AuthToken))] + #[arg(help = "Use the given Sentry auth token")] + pub(super) auth_token: Option, + + #[arg(global=true, ignore_case=true, value_parser=["trace", "debug", "info", "warn", "error"])] + #[arg(long, help = "Set the log output verbosity")] + pub(super) log_level: Option, + + #[arg(global=true, action=SetTrue, visible_alias="silent", long)] + #[arg(help = "Do not print any output while preserving correct exit code. \ + This flag is currently implemented only for selected subcommands")] + pub(super) quiet: bool, + + #[arg(global=true, action=SetTrue, long, hide=true, help="Always return 0 exit code")] + pub(super) allow_failure: bool, +} + +#[derive(Subcommand)] +pub(super) enum SentryCLICommand { + SendMetric(SendMetricArgs), +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 26339c6206..8e31b6ed83 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -18,6 +18,8 @@ use crate::utils::logging::Logger; use crate::utils::system::{init_backtrace, load_dotenv, print_error, QuietExit}; use crate::utils::update::run_sentrycli_update_nagger; +mod derive_parser; + macro_rules! each_subcommand { ($mac:ident) => { $mac!(bash_hook); @@ -36,6 +38,7 @@ macro_rules! each_subcommand { $mac!(repos); $mac!(send_event); $mac!(send_envelope); + $mac!(send_metric); $mac!(sourcemaps); #[cfg(not(feature = "managed"))] $mac!(uninstall); diff --git a/src/commands/send_metric/common_args.rs b/src/commands/send_metric/common_args.rs new file mode 100644 index 0000000000..1d7138cdb1 --- /dev/null +++ b/src/commands/send_metric/common_args.rs @@ -0,0 +1,66 @@ +use crate::utils::value_parsers; +use anyhow::{anyhow, Result}; +use clap::command; +use clap::Args; +use std::str::FromStr; + +/// Arguments for send-metric subcommands using float as value type and no default value. +#[derive(Args)] +pub(super) struct FloatValueMetricArgs { + #[command(flatten)] + pub(super) common: CommonMetricArgs, + + #[arg(short, long, help = "Metric value, any finite 64 bit float.")] + pub(super) value: f64, +} + +/// Common arguments for all send-metric subcommands. +#[derive(Args)] +pub struct CommonMetricArgs { + #[arg(short, long)] + #[arg(help = "Metric name, used for finding the metric on the Sentry UI metrics page.")] + pub name: MetricName, + + #[arg( + short, + long, + help = "Any custom unit. You can have multiple metrics with the same name but different units." + )] + pub unit: Option, + + #[arg(short, long, value_delimiter=',', value_name = "KEY:VALUE", num_args = 1..)] + #[arg(value_parser=value_parsers::kv_parser)] + #[arg( + help = "Metric tags as key:value pairs. Tags are used for filtering on the \ + Sentry UI metrics page." + )] + pub tags: Vec<(String, String)>, +} + +#[derive(Clone)] +pub struct MetricName(String); + +impl MetricName { + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl FromStr for MetricName { + type Err = anyhow::Error; + + /// Metric name must start with an alphabetic character. + fn from_str(s: &str) -> Result { + if s.chars() + .next() + .ok_or_else(|| anyhow!("metric name cannot be empty"))? + .is_ascii_alphabetic() + { + Ok(MetricName(s.to_string())) + } else { + Err(anyhow!( + "metric name must start with an alphabetic character" + )) + } + } +} diff --git a/src/commands/send_metric/distribution.rs b/src/commands/send_metric/distribution.rs new file mode 100644 index 0000000000..0b44ab2ea4 --- /dev/null +++ b/src/commands/send_metric/distribution.rs @@ -0,0 +1,16 @@ +use super::common_args::FloatValueMetricArgs; +use crate::{ + api::envelopes_api::EnvelopesApi, + utils::metrics::{ + normalized_payload::NormalizedPayload, types::MetricType, values::MetricValue, + }, +}; +use anyhow::Result; +use sentry::protocol::EnvelopeItem; + +pub(super) fn execute(args: FloatValueMetricArgs) -> Result<()> { + let value = MetricValue::Float(args.value); + let payload = NormalizedPayload::from_cli_args(&args.common, value, MetricType::Distribution); + EnvelopesApi::try_new()?.send_item(EnvelopeItem::Statsd(payload.to_bytes()?))?; + Ok(()) +} diff --git a/src/commands/send_metric/gauge.rs b/src/commands/send_metric/gauge.rs new file mode 100644 index 0000000000..5d8300554b --- /dev/null +++ b/src/commands/send_metric/gauge.rs @@ -0,0 +1,16 @@ +use super::common_args::FloatValueMetricArgs; +use crate::{ + api::envelopes_api::EnvelopesApi, + utils::metrics::{ + normalized_payload::NormalizedPayload, types::MetricType, values::MetricValue, + }, +}; +use anyhow::Result; +use sentry::protocol::EnvelopeItem; + +pub(super) fn execute(args: FloatValueMetricArgs) -> Result<()> { + let value = MetricValue::Float(args.value); + let payload = NormalizedPayload::from_cli_args(&args.common, value, MetricType::Gauge); + EnvelopesApi::try_new()?.send_item(EnvelopeItem::Statsd(payload.to_bytes()?))?; + Ok(()) +} diff --git a/src/commands/send_metric/increment.rs b/src/commands/send_metric/increment.rs new file mode 100644 index 0000000000..f29c1f86ce --- /dev/null +++ b/src/commands/send_metric/increment.rs @@ -0,0 +1,31 @@ +use super::common_args::CommonMetricArgs; +use crate::{ + api::envelopes_api::EnvelopesApi, + utils::metrics::{ + normalized_payload::NormalizedPayload, types::MetricType, values::MetricValue, + }, +}; +use anyhow::Result; +use clap::{command, Args}; +use sentry::protocol::EnvelopeItem; + +#[derive(Args)] +pub(super) struct IncrementMetricArgs { + #[command(flatten)] + common: CommonMetricArgs, + + #[arg( + short, + long, + help = "Metric value, any finite 64 bit float.", + default_value = "1" + )] + value: f64, +} + +pub(super) fn execute(args: IncrementMetricArgs) -> Result<()> { + let value = MetricValue::Float(args.value); + let payload = NormalizedPayload::from_cli_args(&args.common, value, MetricType::Counter); + EnvelopesApi::try_new()?.send_item(EnvelopeItem::Statsd(payload.to_bytes()?))?; + Ok(()) +} diff --git a/src/commands/send_metric/mod.rs b/src/commands/send_metric/mod.rs new file mode 100644 index 0000000000..906dd4308a --- /dev/null +++ b/src/commands/send_metric/mod.rs @@ -0,0 +1,57 @@ +pub mod common_args; + +mod distribution; +mod gauge; +mod increment; +mod set; + +use self::common_args::FloatValueMetricArgs; +use self::increment::IncrementMetricArgs; +use self::set::SetMetricArgs; +use super::derive_parser::{SentryCLI, SentryCLICommand}; +use anyhow::Result; +use clap::{command, Args, Subcommand}; +use clap::{ArgMatches, Command, Parser}; + +#[derive(Args)] +pub(super) struct SendMetricArgs { + #[command(subcommand)] + subcommand: SendMetricSubcommand, +} + +#[derive(Subcommand)] +#[command(about = "Send a metric to Sentry.")] +#[command(long_about = "Send a metric event to Sentry.{n}{n}\ +This command will validate input parameters and attempt to send a metric to \ +Sentry. Due to network errors and rate limits, the metric is not guaranteed to \ +arrive. Check the debug output for transmission errors by passing --log-level=\ +debug or setting `SENTRY_LOG_LEVEL=debug`.")] +enum SendMetricSubcommand { + #[command(about = "Increment a counter metric")] + Increment(IncrementMetricArgs), + #[command(about = "Update a distribution metric with the provided value")] + Distribution(FloatValueMetricArgs), + #[command(about = "Update a gauge metric with the provided value")] + Gauge(FloatValueMetricArgs), + #[command(about = "Update a set metric with the provided value")] + Set(SetMetricArgs), +} + +pub(super) fn make_command(command: Command) -> Command { + SendMetricSubcommand::augment_subcommands(command) +} + +pub(super) fn execute(_: &ArgMatches) -> Result<()> { + // When adding a new subcommand to the derive_parser SentryCLI, replace the line below with the following: + // let subcommand = match SentryCLI::parse().command { + // SentryCLICommand::SendMetric(SendMetricArgs { subcommand }) => subcommand, + // _ => panic!("expected send-metric subcommand"), + // }; + let SentryCLICommand::SendMetric(SendMetricArgs { subcommand }) = SentryCLI::parse().command; + match subcommand { + SendMetricSubcommand::Increment(args) => increment::execute(args), + SendMetricSubcommand::Distribution(args) => distribution::execute(args), + SendMetricSubcommand::Gauge(args) => gauge::execute(args), + SendMetricSubcommand::Set(args) => set::execute(args), + } +} diff --git a/src/commands/send_metric/set.rs b/src/commands/send_metric/set.rs new file mode 100644 index 0000000000..5f0c563220 --- /dev/null +++ b/src/commands/send_metric/set.rs @@ -0,0 +1,44 @@ +use super::common_args::CommonMetricArgs; +use crate::{ + api::envelopes_api::EnvelopesApi, + utils::metrics::{ + normalized_payload::NormalizedPayload, types::MetricType, values::MetricValue, + }, +}; +use anyhow::Result; +use clap::{command, Args}; +use sentry::protocol::EnvelopeItem; + +#[derive(Args)] +pub(super) struct SetMetricArgs { + #[command(flatten)] + common: CommonMetricArgs, + + #[arg( + short, + long, + help = "Value to add to the set. If the set already contains the provided value, the \ + set's unique count will not increase." + )] + value: SetMetricValue, +} + +#[derive(Clone)] +struct SetMetricValue { + value: u32, +} + +impl From for SetMetricValue { + fn from(s: String) -> SetMetricValue { + SetMetricValue { + value: crc32fast::hash(s.as_bytes()), + } + } +} + +pub(super) fn execute(args: SetMetricArgs) -> Result<()> { + let value = MetricValue::UInt(args.value.value); + let payload = NormalizedPayload::from_cli_args(&args.common, value, MetricType::Set); + EnvelopesApi::try_new()?.send_item(EnvelopeItem::Statsd(payload.to_bytes()?))?; + Ok(()) +} diff --git a/src/utils/metrics/mod.rs b/src/utils/metrics/mod.rs new file mode 100644 index 0000000000..7a0e339cec --- /dev/null +++ b/src/utils/metrics/mod.rs @@ -0,0 +1,7 @@ +pub mod normalized_payload; +pub mod types; +pub mod values; + +mod normalized_name; +mod normalized_tags; +mod normalized_unit; diff --git a/src/utils/metrics/normalized_name.rs b/src/utils/metrics/normalized_name.rs new file mode 100644 index 0000000000..d92e39b8aa --- /dev/null +++ b/src/utils/metrics/normalized_name.rs @@ -0,0 +1,43 @@ +use crate::commands::send_metric::common_args::MetricName; +use regex::Regex; +use std::borrow::Cow; + +pub(super) struct NormalizedName<'a> { + name: Cow<'a, str>, +} + +impl<'a> From<&'a MetricName> for NormalizedName<'a> { + fn from(name: &'a MetricName) -> Self { + Self { + name: Regex::new(r"[^a-zA-Z0-9_\-.]") + .expect("Regex should compile") + .replace_all(name.as_str(), "_"), + } + } +} + +impl std::fmt::Display for NormalizedName<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } +} + +#[cfg(test)] +mod test { + use crate::{ + commands::send_metric::common_args::MetricName, + utils::metrics::normalized_name::NormalizedName, + }; + use std::str::FromStr; + + #[test] + fn test_from() { + let expected = "aA1_-.____________"; + + let actual = + NormalizedName::from(&MetricName::from_str("aA1_-./+ö{😀\n\t\r\\| ,").unwrap()) + .to_string(); + + assert_eq!(expected, actual); + } +} diff --git a/src/utils/metrics/normalized_payload.rs b/src/utils/metrics/normalized_payload.rs new file mode 100644 index 0000000000..df134a8960 --- /dev/null +++ b/src/utils/metrics/normalized_payload.rs @@ -0,0 +1,76 @@ +use super::{ + normalized_name::NormalizedName, normalized_tags::NormalizedTags, + normalized_unit::NormalizedUnit, types::MetricType, values::MetricValue, +}; +use crate::commands::send_metric::common_args::CommonMetricArgs; +use anyhow::Result; +use std::time::{SystemTime, UNIX_EPOCH}; + +pub struct NormalizedPayload<'a> { + name: NormalizedName<'a>, + value: MetricValue, + metric_type: MetricType, + unit: NormalizedUnit<'a>, + tags: NormalizedTags, +} + +impl<'a> NormalizedPayload<'a> { + pub fn from_cli_args( + common_args: &'a CommonMetricArgs, + value: MetricValue, + metric_type: MetricType, + ) -> Self { + Self { + name: NormalizedName::from(&common_args.name), + value, + metric_type, + unit: NormalizedUnit::from(&common_args.unit), + tags: NormalizedTags::from(common_args.tags.as_slice()).with_default_tags(), + } + } + + pub fn to_bytes(&self) -> Result> { + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let data = format!( + "{}@{}:{}|{}|#{}|T{}", + self.name, self.unit, self.value, self.metric_type, self.tags, timestamp + ); + Ok(data.into_bytes()) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use crate::{ + commands::send_metric::common_args::{CommonMetricArgs, MetricName}, + config::Config, + utils::metrics::{ + normalized_payload::NormalizedPayload, types::MetricType, values::MetricValue, + }, + }; + use regex::Regex; + + #[test] + fn test_from_cli_args_and_to_bytes() { + Config::from_cli_config().unwrap().bind_to_process(); + let common_args = CommonMetricArgs { + name: MetricName::from_str("nöme").unwrap(), + unit: Some("möb".to_string()), + tags: vec![("atagö".to_string(), "aval|ö".to_string())], + }; + let expected = Regex::new( + r"^n_me@mb:1\|s\|#atag:aval\\u\{7c}ö,environment:production,release:.+\|T\d{10}$", + ) + .unwrap(); + + let bytes = + NormalizedPayload::from_cli_args(&common_args, MetricValue::UInt(1), MetricType::Set) + .to_bytes() + .unwrap(); + let actual = String::from_utf8_lossy(&bytes); + + assert!(expected.is_match(&actual)); + } +} diff --git a/src/utils/metrics/normalized_tags.rs b/src/utils/metrics/normalized_tags.rs new file mode 100644 index 0000000000..ad154715ca --- /dev/null +++ b/src/utils/metrics/normalized_tags.rs @@ -0,0 +1,171 @@ +use crate::{config::Config, utils::releases}; +use itertools::Itertools; +use regex::Regex; +use std::collections::HashMap; +use unicode_segmentation::UnicodeSegmentation; + +pub(super) struct NormalizedTags { + tags: HashMap, +} + +impl From<&[(String, String)]> for NormalizedTags { + fn from(tags: &[(String, String)]) -> Self { + Self { + tags: tags + .iter() + .map(|(k, v)| (Self::normalize_key(k), Self::normalize_value(v))) + .filter(|(k, v)| !v.is_empty() && !k.is_empty()) + .collect(), + } + } +} + +impl NormalizedTags { + fn normalize_key(key: &str) -> String { + Regex::new(r"[^a-zA-Z0-9_\-./]") + .expect("Tag normalization regex should compile") + .replace_all(&key.graphemes(true).take(32).collect::(), "") + .to_string() + } + + fn normalize_value(value: &str) -> String { + value + .graphemes(true) + .take(200) + .collect::() + .replace('\\', "\\\\") + .replace('\n', "\\n") + .replace('\r', "\\r") + .replace('\t', "\\t") + .replace('|', "\\u{7c}") + .replace(',', "\\u{2c}") + } + + pub(super) fn with_default_tags(mut self) -> Self { + if let Ok(release) = releases::detect_release_name() { + self.tags + .entry("release".to_string()) + .or_insert(Self::normalize_value(&release)); + } + self.tags + .entry("environment".to_string()) + .or_insert(Self::normalize_value( + &Config::current() + .get_environment() + .unwrap_or("production".to_string()), + )); + self + } +} + +impl std::fmt::Display for NormalizedTags { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let res = self + .tags + .iter() + .map(|(k, v)| format!("{}:{}", k, v)) + .sorted() + .join(","); + write!(f, "{res}") + } +} + +#[cfg(test)] +mod test { + use std::env; + + use crate::config::Config; + + use super::NormalizedTags; + + #[test] + fn test_replacement_characters() { + let tags: Vec<(String, String)> = [ + ("a\na", "a\na"), + ("b\rb", "b\rb"), + ("c\tc", "c\tc"), + ("d\\d", "d\\d"), + ("e|e", "e|e"), + ("f,f", "f,f"), + ] + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + let expected = "aa:a\\na,bb:b\\rb,cc:c\\tc,dd:d\\\\d,ee:e\\u{7c}e,ff:f\\u{2c}f"; + + let actual = NormalizedTags::from(tags.as_slice()).to_string(); + + assert_eq!(expected, actual); + } + + #[test] + fn test_empty_tags() { + let tags: Vec<(String, String)> = [("+", "a"), ("a", ""), ("", "a"), ("", "")] + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + let expected = ""; + + let actual = NormalizedTags::from(tags.as_slice()).to_string(); + + assert_eq!(expected, actual); + } + + #[test] + fn test_special_characters() { + let tags: Vec<(String, String)> = [("aA1_-./+ö{ 😀", "aA1_-./+ö{ 😀")] + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + let expected = "aA1_-./:aA1_-./+ö{ 😀"; + + let actual = NormalizedTags::from(tags.as_slice()).to_string(); + + assert_eq!(expected, actual); + } + + #[test] + fn test_add_default_tags() { + Config::from_cli_config().unwrap().bind_to_process(); + env::set_var("SOURCE_VERSION", "my-release"); + let expected = "environment:production,release:my-release"; + + let actual = NormalizedTags::from(Vec::new().as_slice()) + .with_default_tags() + .to_string(); + + assert_eq!(expected, actual); + } + + #[test] + fn test_override_default_tags() { + Config::from_cli_config().unwrap().bind_to_process(); + env::set_var("SOURCE_VERSION", "my-release"); + let expected = "environment:env_override,release:release_override"; + + let actual = NormalizedTags::from( + vec![ + ("release".to_string(), "release_override".to_string()), + ("environment".to_string(), "env_override".to_string()), + ] + .as_slice(), + ) + .with_default_tags() + .to_string(); + + assert_eq!(expected, actual); + } + + #[test] + fn test_tag_lengths() { + let expected = "abcdefghijklmnopqrstuvwxyzabcde:abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopq🙂"; + + let actual = NormalizedTags::from(vec![ + ("abcdefghijklmnopqrstuvwxyzabcde🙂fghijklmnopqrstuvwxyz".to_string(), + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopq🙂rstuvwxyz".to_string()), + ].as_slice()) + .to_string(); + + assert_eq!(expected, actual); + } +} diff --git a/src/utils/metrics/normalized_unit.rs b/src/utils/metrics/normalized_unit.rs new file mode 100644 index 0000000000..48f47bb362 --- /dev/null +++ b/src/utils/metrics/normalized_unit.rs @@ -0,0 +1,65 @@ +use regex::Regex; +use std::borrow::Cow; + +pub(super) struct NormalizedUnit<'a> { + unit: Cow<'a, str>, +} + +impl<'a> From<&'a Option> for NormalizedUnit<'a> { + fn from(unit: &'a Option) -> Self { + let safe_unit = match unit { + Some(unit) => Regex::new(r"[^a-zA-Z0-9_]") + .expect("Regex should compile") + .replace_all(unit, ""), + None => Cow::from(""), + }; + Self { + unit: if safe_unit.is_empty() { + Cow::from("none") + } else { + safe_unit + }, + } + } +} + +impl std::fmt::Display for NormalizedUnit<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.unit) + } +} + +#[cfg(test)] +mod test { + use crate::utils::metrics::normalized_unit::NormalizedUnit; + + #[test] + fn test_from() { + let unit = Some("aA1_-./+ö{😀\n\t\r\\| ,".to_string()); + let expected = "aA1_"; + + let actual = NormalizedUnit::from(&unit).to_string(); + + assert_eq!(expected, actual); + } + + #[test] + fn test_from_empty() { + let unit = Some("".to_string()); + let expected = "none"; + + let actual = NormalizedUnit::from(&unit).to_string(); + + assert_eq!(expected, actual); + } + + #[test] + fn test_from_empty_after_normalization() { + let unit = Some("+".to_string()); + let expected = "none"; + + let actual = NormalizedUnit::from(&unit).to_string(); + + assert_eq!(expected, actual); + } +} diff --git a/src/utils/metrics/types.rs b/src/utils/metrics/types.rs new file mode 100644 index 0000000000..ab33c1925a --- /dev/null +++ b/src/utils/metrics/types.rs @@ -0,0 +1,20 @@ +use core::fmt; +use std::fmt::{Display, Formatter}; + +pub enum MetricType { + Gauge, + Distribution, + Counter, + Set, +} + +impl Display for MetricType { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + MetricType::Gauge => write!(f, "g"), + MetricType::Distribution => write!(f, "d"), + MetricType::Counter => write!(f, "c"), + MetricType::Set => write!(f, "s"), + } + } +} diff --git a/src/utils/metrics/values.rs b/src/utils/metrics/values.rs new file mode 100644 index 0000000000..c5608e71b8 --- /dev/null +++ b/src/utils/metrics/values.rs @@ -0,0 +1,16 @@ +use core::fmt; +use std::fmt::{Display, Formatter}; + +pub enum MetricValue { + UInt(u32), + Float(f64), +} + +impl Display for MetricValue { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + MetricValue::UInt(value) => write!(f, "{value}"), + MetricValue::Float(value) => write!(f, "{value}"), + } + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 3f7704d273..3a7930f8dc 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -14,6 +14,7 @@ pub mod formatting; pub mod fs; pub mod http; pub mod logging; +pub mod metrics; pub mod progress; pub mod releases; pub mod retry; @@ -21,5 +22,6 @@ pub mod sourcemaps; pub mod system; pub mod ui; pub mod update; +pub mod value_parsers; pub mod vcs; pub mod xcode; diff --git a/src/utils/value_parsers.rs b/src/utils/value_parsers.rs new file mode 100644 index 0000000000..bdfd2d5309 --- /dev/null +++ b/src/utils/value_parsers.rs @@ -0,0 +1,12 @@ +use anyhow::{anyhow, Result}; + +/// Parse key:value pair from string, used as a value_parser for Clap arguments +pub fn kv_parser(s: &str) -> Result<(String, String)> { + let pos = s + .find(':') + .ok_or_else(|| anyhow!(format!("`{s}` is missing a `:`")))?; + Ok(( + s[..pos].parse().expect("infallible"), + s[pos + 1..].parse().expect("infallible"), + )) +} diff --git a/tests/integration/_cases/help/help-windows.trycmd b/tests/integration/_cases/help/help-windows.trycmd index abc5a066d4..216691e7f1 100644 --- a/tests/integration/_cases/help/help-windows.trycmd +++ b/tests/integration/_cases/help/help-windows.trycmd @@ -26,6 +26,7 @@ Commands: repos Manage repositories on Sentry. send-event Send a manual event to Sentry. send-envelope Send a stored envelope to Sentry. + send-metric Send a metric to Sentry. sourcemaps Manage sourcemaps for Sentry releases. upload-proguard Upload ProGuard mapping files to a project. help Print this message or the help of the given subcommand(s) diff --git a/tests/integration/_cases/help/help.trycmd b/tests/integration/_cases/help/help.trycmd index 62e5ef0829..d3b5ecd539 100644 --- a/tests/integration/_cases/help/help.trycmd +++ b/tests/integration/_cases/help/help.trycmd @@ -26,6 +26,7 @@ Commands: repos Manage repositories on Sentry. send-event Send a manual event to Sentry. send-envelope Send a stored envelope to Sentry. + send-metric Send a metric to Sentry. sourcemaps Manage sourcemaps for Sentry releases. uninstall Uninstall the sentry-cli executable. upload-proguard Upload ProGuard mapping files to a project. diff --git a/tests/integration/_cases/send_metric/send_metric-distribution-all-options-long-with-float-value.trycmd b/tests/integration/_cases/send_metric/send_metric-distribution-all-options-long-with-float-value.trycmd new file mode 100644 index 0000000000..cd1f21bca5 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-distribution-all-options-long-with-float-value.trycmd @@ -0,0 +1,10 @@ +``` +$ sentry-cli send-metric distribution --name testMetric --value 2.3 --unit mb --tags a:b --log-level debug +? success +... +{} +{"type":"statsd","length":[..]} +testMetric@mb:2.3|d|#a:b,environment:production,release:[..]|T[..] +... + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-distribution-all-options-short-with-int-value.trycmd b/tests/integration/_cases/send_metric/send_metric-distribution-all-options-short-with-int-value.trycmd new file mode 100644 index 0000000000..2db60885db --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-distribution-all-options-short-with-int-value.trycmd @@ -0,0 +1,10 @@ +``` +$ sentry-cli send-metric distribution -n testMetric -v 2 -u mb -t a:b --log-level debug +? success +... +{} +{"type":"statsd","length":[..]} +testMetric@mb:2|d|#a:b,environment:production,release:[..]|T[..] +... + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-distribution-help.trycmd b/tests/integration/_cases/send_metric/send_metric-distribution-help.trycmd new file mode 100644 index 0000000000..a3f2c2cfe9 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-distribution-help.trycmd @@ -0,0 +1,26 @@ +``` +$ sentry-cli send-metric distribution --help +? success +Update a distribution metric with the provided value + +Usage: sentry-cli[EXE] send-metric distribution [OPTIONS] --name --value + +Options: + -n, --name Metric name, used for finding the metric on the Sentry UI metrics + page. + --header Custom headers that should be attached to all requests + in key:value format. + -u, --unit Any custom unit. You can have multiple metrics with the same name + but different units. + --auth-token Use the given Sentry auth token. + -t, --tags ... Metric tags as key:value pairs. Tags are used for filtering on the + Sentry UI metrics page. + -v, --value Metric value, any finite 64 bit float. + --log-level Set the log output verbosity. [possible values: trace, debug, info, + warn, error] + --quiet Do not print any output while preserving correct exit code. This + flag is currently implemented only for selected subcommands. + [aliases: silent] + -h, --help Print help + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-distribution-no-options.trycmd b/tests/integration/_cases/send_metric/send_metric-distribution-no-options.trycmd new file mode 100644 index 0000000000..0b64c89e97 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-distribution-no-options.trycmd @@ -0,0 +1,12 @@ +``` +$ sentry-cli send-metric distribution +? failed +error: the following required arguments were not provided: + --name + --value + +Usage: sentry-cli[EXE] send-metric distribution --name --value + +For more information, try '--help'. + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-distribution-normalization.trycmd b/tests/integration/_cases/send_metric/send_metric-distribution-normalization.trycmd new file mode 100644 index 0000000000..f80d621cf4 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-distribution-normalization.trycmd @@ -0,0 +1,10 @@ +``` +$ sentry-cli --log-level debug send-metric distribution -v 1 -n "aA1_-./+ö{😀 |\\," -u "aA1_-./+ö{😀 |\\," -t "aA1_-./+ö{😀 |\\:aA1_-./+ö{😀 |\\" release:testRelease +:b +:+ a: :b : +? success +... +{} +{"type":"statsd","length":[..]} +aA1_-.__________@aA1_:1|d|#aA1_-./:aA1_-./+ö{😀 /t/u{7c}//,environment:production,release:testRelease|T[..] +... + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-distribution-numerical-key-prefix.trycmd b/tests/integration/_cases/send_metric/send_metric-distribution-numerical-key-prefix.trycmd new file mode 100644 index 0000000000..8451c5f63b --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-distribution-numerical-key-prefix.trycmd @@ -0,0 +1,8 @@ +``` +$ sentry-cli --log-level debug send-metric distribution -v 1 -n 1mymetric +? failed +error: invalid value '1mymetric' for '--name ': metric name must start with an alphabetic character + +For more information, try '--help'. + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-distribution-required-options-with-negative-value.trycmd b/tests/integration/_cases/send_metric/send_metric-distribution-required-options-with-negative-value.trycmd new file mode 100644 index 0000000000..aea6b25b25 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-distribution-required-options-with-negative-value.trycmd @@ -0,0 +1,10 @@ +``` +$ sentry-cli send-metric distribution -n testMetric -v=-3.2 --log-level debug +? success +... +{} +{"type":"statsd","length":[..]} +testMetric@none:-3.2|d|#environment:production,release:[..]|T[..] +... + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-gauge-all-options-long-with-float-value.trycmd b/tests/integration/_cases/send_metric/send_metric-gauge-all-options-long-with-float-value.trycmd new file mode 100644 index 0000000000..021a8d6938 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-gauge-all-options-long-with-float-value.trycmd @@ -0,0 +1,10 @@ +``` +$ sentry-cli send-metric gauge --name testMetric --value 2.3 --unit mb --tags a:b --log-level debug +? success +... +{} +{"type":"statsd","length":[..]} +testMetric@mb:2.3|g|#a:b,environment:production,release:[..]|T[..] +... + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-gauge-all-options-short-with-int-value.trycmd b/tests/integration/_cases/send_metric/send_metric-gauge-all-options-short-with-int-value.trycmd new file mode 100644 index 0000000000..c81d7cf8d3 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-gauge-all-options-short-with-int-value.trycmd @@ -0,0 +1,10 @@ +``` +$ sentry-cli send-metric gauge -n testMetric -v 2 -u mb -t a:b --log-level debug +? success +... +{} +{"type":"statsd","length":[..]} +testMetric@mb:2|g|#a:b,environment:production,release:[..]|T[..] +... + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-gauge-help.trycmd b/tests/integration/_cases/send_metric/send_metric-gauge-help.trycmd new file mode 100644 index 0000000000..f2355eab25 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-gauge-help.trycmd @@ -0,0 +1,26 @@ +``` +$ sentry-cli send-metric gauge --help +? success +Update a gauge metric with the provided value + +Usage: sentry-cli[EXE] send-metric gauge [OPTIONS] --name --value + +Options: + -n, --name Metric name, used for finding the metric on the Sentry UI metrics + page. + --header Custom headers that should be attached to all requests + in key:value format. + -u, --unit Any custom unit. You can have multiple metrics with the same name + but different units. + --auth-token Use the given Sentry auth token. + -t, --tags ... Metric tags as key:value pairs. Tags are used for filtering on the + Sentry UI metrics page. + -v, --value Metric value, any finite 64 bit float. + --log-level Set the log output verbosity. [possible values: trace, debug, info, + warn, error] + --quiet Do not print any output while preserving correct exit code. This + flag is currently implemented only for selected subcommands. + [aliases: silent] + -h, --help Print help + +``` \ No newline at end of file diff --git a/tests/integration/_cases/send_metric/send_metric-gauge-no-options.trycmd b/tests/integration/_cases/send_metric/send_metric-gauge-no-options.trycmd new file mode 100644 index 0000000000..a5a791d61d --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-gauge-no-options.trycmd @@ -0,0 +1,12 @@ +``` +$ sentry-cli send-metric gauge +? failed +error: the following required arguments were not provided: + --name + --value + +Usage: sentry-cli[EXE] send-metric gauge --name --value + +For more information, try '--help'. + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-gauge-normalization.trycmd b/tests/integration/_cases/send_metric/send_metric-gauge-normalization.trycmd new file mode 100644 index 0000000000..9a3d1ea05f --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-gauge-normalization.trycmd @@ -0,0 +1,10 @@ +``` +$ sentry-cli --log-level debug send-metric gauge -v 1 -n "aA1_-./+ö{😀 |\\," -u "aA1_-./+ö{😀 |\\," -t "aA1_-./+ö{😀 |\\:aA1_-./+ö{😀 |\\" release:testRelease +:b +:+ a: :b : +? success +... +{} +{"type":"statsd","length":[..]} +aA1_-.__________@aA1_:1|g|#aA1_-./:aA1_-./+ö{😀 /t/u{7c}//,environment:production,release:testRelease|T[..] +... + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-gauge-numerical-key-prefix.trycmd b/tests/integration/_cases/send_metric/send_metric-gauge-numerical-key-prefix.trycmd new file mode 100644 index 0000000000..25a0e7c00b --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-gauge-numerical-key-prefix.trycmd @@ -0,0 +1,8 @@ +``` +$ sentry-cli --log-level debug send-metric gauge -v 1 -n 1mymetric +? failed +error: invalid value '1mymetric' for '--name ': metric name must start with an alphabetic character + +For more information, try '--help'. + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-gauge-required-options-with-negative-value.trycmd b/tests/integration/_cases/send_metric/send_metric-gauge-required-options-with-negative-value.trycmd new file mode 100644 index 0000000000..ea027e0965 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-gauge-required-options-with-negative-value.trycmd @@ -0,0 +1,10 @@ +``` +$ sentry-cli send-metric gauge -n testMetric -v=-3.2 --log-level debug +? success +... +{} +{"type":"statsd","length":[..]} +testMetric@none:-3.2|g|#environment:production,release:[..]|T[..] +... + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-global-options.trycmd b/tests/integration/_cases/send_metric/send_metric-global-options.trycmd new file mode 100644 index 0000000000..2ebd6e00b1 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-global-options.trycmd @@ -0,0 +1,14 @@ +``` +$ sentry-cli send-metric --header a:b --auth-token "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" --allow-failure --quiet --log-level debug +? failed + INFO [..] Loaded config from [CWD]/.sentryclirc + DEBUG [..] sentry-cli version: [VERSION], platform: [..], architecture: [..] + INFO [..] sentry-cli was invoked with the following command line: "[CWD]/target/debug/sentry-cli[EXE]" "send-metric" "--header" "a:b" "--auth-token" "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" "--allow-failure" "--quiet" "--log-level" "debug" +error: 'sentry-cli[EXE] send-metric' requires a subcommand but one was not provided + [subcommands: increment, distribution, gauge, set, help] + +Usage: sentry-cli[EXE] send-metric [OPTIONS] + +For more information, try '--help'. + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-help.trycmd b/tests/integration/_cases/send_metric/send_metric-help.trycmd new file mode 100644 index 0000000000..ae5452ff41 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-help.trycmd @@ -0,0 +1,46 @@ +``` +$ sentry-cli send-metric --help +? success +Send a metric event to Sentry. + +This command will validate input parameters and attempt to send a metric to Sentry. Due to network +errors and rate limits, the metric is not guaranteed to arrive. Check the debug output for +transmission errors by passing --log-level=debug or setting `SENTRY_LOG_LEVEL=debug`. + +Usage: sentry-cli[EXE] send-metric [OPTIONS] [COMMAND] + +Commands: + increment + Increment a counter metric + distribution + Update a distribution metric with the provided value + gauge + Update a gauge metric with the provided value + set + Update a set metric with the provided value + help + Print this message or the help of the given subcommand(s) + +Options: + --header + Custom headers that should be attached to all requests + in key:value format. + + --auth-token + Use the given Sentry auth token. + + --log-level + Set the log output verbosity. + + [possible values: trace, debug, info, warn, error] + + --quiet + Do not print any output while preserving correct exit code. This flag is currently + implemented only for selected subcommands. + + [aliases: silent] + + -h, --help + Print help (see a summary with '-h') + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-increment-all-options-long-with-float-value.trycmd b/tests/integration/_cases/send_metric/send_metric-increment-all-options-long-with-float-value.trycmd new file mode 100644 index 0000000000..fbd50d4dbd --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-increment-all-options-long-with-float-value.trycmd @@ -0,0 +1,10 @@ +``` +$ sentry-cli send-metric increment --name testMetric --value 2.3 --unit mb --tags a:b --log-level debug +? success +... +{} +{"type":"statsd","length":[..]} +testMetric@mb:2.3|c|#a:b,environment:production,release:[..]|T[..] +... + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-increment-all-options-short-with-int-value.trycmd b/tests/integration/_cases/send_metric/send_metric-increment-all-options-short-with-int-value.trycmd new file mode 100644 index 0000000000..bd5fd53205 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-increment-all-options-short-with-int-value.trycmd @@ -0,0 +1,10 @@ +``` +$ sentry-cli send-metric increment -n testMetric -v 2 -u mb -t a:b --log-level debug +? success +... +{} +{"type":"statsd","length":[..]} +testMetric@mb:2|c|#a:b,environment:production,release:[..]|T[..] +... + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-increment-help.trycmd b/tests/integration/_cases/send_metric/send_metric-increment-help.trycmd new file mode 100644 index 0000000000..f5fe209d22 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-increment-help.trycmd @@ -0,0 +1,26 @@ +``` +$ sentry-cli send-metric increment --help +? success +Increment a counter metric + +Usage: sentry-cli[EXE] send-metric increment [OPTIONS] --name + +Options: + -n, --name Metric name, used for finding the metric on the Sentry UI metrics + page. + --header Custom headers that should be attached to all requests + in key:value format. + -u, --unit Any custom unit. You can have multiple metrics with the same name + but different units. + --auth-token Use the given Sentry auth token. + -t, --tags ... Metric tags as key:value pairs. Tags are used for filtering on the + Sentry UI metrics page. + -v, --value Metric value, any finite 64 bit float. [default: 1] + --log-level Set the log output verbosity. [possible values: trace, debug, info, + warn, error] + --quiet Do not print any output while preserving correct exit code. This + flag is currently implemented only for selected subcommands. + [aliases: silent] + -h, --help Print help + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-increment-no-dsn.trycmd b/tests/integration/_cases/send_metric/send_metric-increment-no-dsn.trycmd new file mode 100644 index 0000000000..87fec5a093 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-increment-no-dsn.trycmd @@ -0,0 +1,10 @@ +``` +$ sentry-cli send-metric increment -n testmetric +? failed +... +error: DSN missing. Please set the `SENTRY_DSN` environment variable to your project's DSN. + +Add --log-level=[info|debug] or export SENTRY_LOG_LEVEL=[info|debug] to see more output. +Please attach the full debug log to all bug reports. + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-increment-no-options.trycmd b/tests/integration/_cases/send_metric/send_metric-increment-no-options.trycmd new file mode 100644 index 0000000000..da03248fce --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-increment-no-options.trycmd @@ -0,0 +1,11 @@ +``` +$ sentry-cli send-metric increment +? failed +error: the following required arguments were not provided: + --name + +Usage: sentry-cli[EXE] send-metric increment --name + +For more information, try '--help'. + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-increment-normalization-with-negative-value.trycmd b/tests/integration/_cases/send_metric/send_metric-increment-normalization-with-negative-value.trycmd new file mode 100644 index 0000000000..7aeae1624c --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-increment-normalization-with-negative-value.trycmd @@ -0,0 +1,10 @@ +``` +$ sentry-cli --log-level debug send-metric increment -v=-3 -n "aA1_-./+ö{😀 |\\," -u "aA1_-./+ö{😀 |\\," -t "aA1_-./+ö{😀 |\\:aA1_-./+ö{😀 |\\" release:testRelease +:b +:+ a: :b : +? success +... +{} +{"type":"statsd","length":[..]} +aA1_-.__________@aA1_:-3|c|#aA1_-./:aA1_-./+ö{😀 /t/u{7c}//,environment:production,release:testRelease|T[..] +... + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-increment-numerical-key-prefix.trycmd b/tests/integration/_cases/send_metric/send_metric-increment-numerical-key-prefix.trycmd new file mode 100644 index 0000000000..496cbc39ec --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-increment-numerical-key-prefix.trycmd @@ -0,0 +1,8 @@ +``` +$ sentry-cli --log-level debug send-metric increment -v 1 -n 1mymetric +? failed +error: invalid value '1mymetric' for '--name ': metric name must start with an alphabetic character + +For more information, try '--help'. + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-increment-required-options.trycmd b/tests/integration/_cases/send_metric/send_metric-increment-required-options.trycmd new file mode 100644 index 0000000000..6e22cfaea3 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-increment-required-options.trycmd @@ -0,0 +1,10 @@ +``` +$ sentry-cli send-metric increment -n testMetric --log-level debug +? success +... +{} +{"type":"statsd","length":[..]} +testMetric@none:1|c|#environment:production,release:[..]|T[..] +... + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-increment-tag-no-colon.trycmd b/tests/integration/_cases/send_metric/send_metric-increment-tag-no-colon.trycmd new file mode 100644 index 0000000000..249c39edf9 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-increment-tag-no-colon.trycmd @@ -0,0 +1,8 @@ +``` +$ sentry-cli send-metric increment -n testmetric -t ab,release:testRelease --log-level debug +? failed +error: invalid value 'ab' for '--tags ...': `ab` is missing a `:` + +For more information, try '--help'. + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-increment-unsuccessful-api-call.trycmd b/tests/integration/_cases/send_metric/send_metric-increment-unsuccessful-api-call.trycmd new file mode 100644 index 0000000000..d8dacf11fd --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-increment-unsuccessful-api-call.trycmd @@ -0,0 +1,10 @@ +``` +$ sentry-cli send-metric increment -n testmetric +? failed +error: API request failed + caused by: sentry reported an error: internal server error (http status: 500) + +Add --log-level=[info|debug] or export SENTRY_LOG_LEVEL=[info|debug] to see more output. +Please attach the full debug log to all bug reports. + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-no-subcommand.trycmd b/tests/integration/_cases/send_metric/send_metric-no-subcommand.trycmd new file mode 100644 index 0000000000..84d871d095 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-no-subcommand.trycmd @@ -0,0 +1,26 @@ +``` +$ sentry-cli send-metric +? failed +Send a metric to Sentry. + +Usage: sentry-cli[EXE] send-metric [OPTIONS] + +Commands: + increment Increment a counter metric + distribution Update a distribution metric with the provided value + gauge Update a gauge metric with the provided value + set Update a set metric with the provided value + help Print this message or the help of the given subcommand(s) + +Options: + --header Custom headers that should be attached to all requests + in key:value format + --auth-token Use the given Sentry auth token + --log-level Set the log output verbosity [possible values: trace, debug, info, + warn, error] + --quiet Do not print any output while preserving correct exit code. This + flag is currently implemented only for selected subcommands + [aliases: silent] + -h, --help Print help (see more with '--help') + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-set-all-options-long-with-alphabetic-value.trycmd b/tests/integration/_cases/send_metric/send_metric-set-all-options-long-with-alphabetic-value.trycmd new file mode 100644 index 0000000000..5d76c42e20 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-set-all-options-long-with-alphabetic-value.trycmd @@ -0,0 +1,10 @@ +``` +$ sentry-cli --log-level debug send-metric set --name testMetric --value abc --unit mb --tags a:b +? success +... +{} +{"type":"statsd","length":[..]} +testMetric@mb:891568578|s|#a:b,environment:production,release:[..]|T[..] +... + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-set-all-options-short-with-int-value.trycmd b/tests/integration/_cases/send_metric/send_metric-set-all-options-short-with-int-value.trycmd new file mode 100644 index 0000000000..9ffe0110c1 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-set-all-options-short-with-int-value.trycmd @@ -0,0 +1,10 @@ +``` +$ sentry-cli --log-level debug send-metric set -n testMetric -v 2 -u mb -t a:b,environment:testEnvironment,release:testRelease +? success +... +{} +{"type":"statsd","length":[..]} +testMetric@mb:450215437|s|#a:b,environment:testEnvironment,release:testRelease|T[..] +... + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-set-float-value.trycmd b/tests/integration/_cases/send_metric/send_metric-set-float-value.trycmd new file mode 100644 index 0000000000..b0de3c82e5 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-set-float-value.trycmd @@ -0,0 +1,10 @@ +``` +$ sentry-cli send-metric set -n testmetric -v 1.9 --log-level debug +? success +... +{} +{"type":"statsd","length":113} +testmetric@none:2397755281|s|#environment:production,release:[..]|T[..] +... + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-set-help.trycmd b/tests/integration/_cases/send_metric/send_metric-set-help.trycmd new file mode 100644 index 0000000000..4d08b4bfc2 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-set-help.trycmd @@ -0,0 +1,27 @@ +``` +$ sentry-cli send-metric set --help +? success +Update a set metric with the provided value + +Usage: sentry-cli[EXE] send-metric set [OPTIONS] --name --value + +Options: + -n, --name Metric name, used for finding the metric on the Sentry UI metrics + page. + --header Custom headers that should be attached to all requests + in key:value format. + -u, --unit Any custom unit. You can have multiple metrics with the same name + but different units. + --auth-token Use the given Sentry auth token. + -t, --tags ... Metric tags as key:value pairs. Tags are used for filtering on the + Sentry UI metrics page. + -v, --value Value to add to the set. If the set already contains the provided + value, the set's unique count will not increase. + --log-level Set the log output verbosity. [possible values: trace, debug, info, + warn, error] + --quiet Do not print any output while preserving correct exit code. This + flag is currently implemented only for selected subcommands. + [aliases: silent] + -h, --help Print help + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-set-no-options.trycmd b/tests/integration/_cases/send_metric/send_metric-set-no-options.trycmd new file mode 100644 index 0000000000..5f49058130 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-set-no-options.trycmd @@ -0,0 +1,12 @@ +``` +$ sentry-cli send-metric set +? failed +error: the following required arguments were not provided: + --name + --value + +Usage: sentry-cli[EXE] send-metric set --name --value + +For more information, try '--help'. + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-set-normalization.trycmd b/tests/integration/_cases/send_metric/send_metric-set-normalization.trycmd new file mode 100644 index 0000000000..7c7110ffa1 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-set-normalization.trycmd @@ -0,0 +1,10 @@ +``` +$ sentry-cli --log-level debug send-metric set -v 1 -n "aA1_-./+ö{😀 |\\," -u "aA1_-./+ö{😀 |\\," -t "aA1_-./+ö{😀 |\\:aA1_-./+ö{😀 |\\" release:testRelease +:b +:+ a: :b : +? success +... +{} +{"type":"statsd","length":[..]} +aA1_-.__________@aA1_:2212294583|s|#aA1_-./:aA1_-./+ö{😀 /t/u{7c}//,environment:production,release:testRelease|T[..] +... + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-set-numerical-key-prefix.trycmd b/tests/integration/_cases/send_metric/send_metric-set-numerical-key-prefix.trycmd new file mode 100644 index 0000000000..7823e55db8 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-set-numerical-key-prefix.trycmd @@ -0,0 +1,8 @@ +``` +$ sentry-cli --log-level debug send-metric set -v 1 -n 1mymetric +? failed +error: invalid value '1mymetric' for '--name ': metric name must start with an alphabetic character + +For more information, try '--help'. + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-set-required-options-with-negative-value.trycmd b/tests/integration/_cases/send_metric/send_metric-set-required-options-with-negative-value.trycmd new file mode 100644 index 0000000000..bde4cb9642 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-set-required-options-with-negative-value.trycmd @@ -0,0 +1,10 @@ +``` +$ sentry-cli send-metric set -n testMetric -v=-3 --log-level debug +? success +... +{} +{"type":"statsd","length":[..]} +testMetric@none:3726846214|s|#environment:production,release:[..]|T[..] +... + +``` diff --git a/tests/integration/mod.rs b/tests/integration/mod.rs index 0387b6c869..0b99a9099e 100644 --- a/tests/integration/mod.rs +++ b/tests/integration/mod.rs @@ -15,6 +15,7 @@ mod react_native; mod releases; mod send_envelope; mod send_event; +mod send_metric; mod sourcemaps; mod token_validation; mod uninstall; @@ -26,7 +27,7 @@ use std::fs; use std::io; use std::path::Path; -use mockito::{mock, server_url, Matcher, Mock}; +use mockito::{self, mock, server_url, Matcher, Mock}; use trycmd::TestCases; pub const UTC_DATE_FORMAT: &str = r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{6,9}Z"; @@ -34,13 +35,14 @@ const VERSION: &str = env!("CARGO_PKG_VERSION"); pub fn register_test_without_token(path: &str) -> TestCases { let test_case = TestCases::new(); + let server_addr = mockito::server_address(); test_case .env("SENTRY_INTEGRATION_TEST", "1") .env("SENTRY_DUMP_RESPONSES", "dump") // reused default directory of `trycmd` output dumps .env("SENTRY_URL", server_url()) .env("SENTRY_ORG", "wat-org") .env("SENTRY_PROJECT", "wat-project") - .env("SENTRY_DSN", format!("https://test@{}/1337", server_url())) + .env("SENTRY_DSN", format!("http://test@{}/1337", server_addr)) .case(format!("tests/integration/_cases/{path}")); test_case.insert_var("[VERSION]", VERSION).unwrap(); test_case @@ -60,6 +62,7 @@ pub struct EndpointOptions { pub response_body: Option, pub response_file: Option, pub matcher: Option, + pub header_matcher: Option<(&'static str, Matcher)>, } impl EndpointOptions { @@ -71,6 +74,7 @@ impl EndpointOptions { response_body: None, response_file: None, matcher: None, + header_matcher: None, } } @@ -91,6 +95,13 @@ impl EndpointOptions { self.matcher = Some(matcher); self } + + /// Matches a header of the mock endpoint. The header must be present and its value must + /// match the provided matcher in order for the endpoint to be reached. + pub fn with_header_matcher(mut self, key: &'static str, matcher: Matcher) -> Self { + self.header_matcher = Some((key, matcher)); + self + } } pub fn mock_endpoint(opts: EndpointOptions) -> Mock { @@ -110,6 +121,10 @@ pub fn mock_endpoint(opts: EndpointOptions) -> Mock { mock = mock.match_body(matcher); } + if let Some((key, matcher)) = opts.header_matcher { + mock = mock.match_header(key, matcher); + } + mock.create() } diff --git a/tests/integration/send_metric/distribution.rs b/tests/integration/send_metric/distribution.rs new file mode 100644 index 0000000000..b4dbc19a0d --- /dev/null +++ b/tests/integration/send_metric/distribution.rs @@ -0,0 +1,49 @@ +use crate::integration; + +#[test] +fn command_send_metric_distribution_all_options_long_with_float_value() { + let _m = super::mock_envelopes_endpoint(); + integration::register_test( + "send_metric/send_metric-distribution-all-options-long-with-float-value.trycmd", + ); +} + +#[test] +fn command_send_metric_distribution_all_options_short_with_int_value() { + let _m = super::mock_envelopes_endpoint(); + integration::register_test( + "send_metric/send_metric-distribution-all-options-short-with-int-value.trycmd", + ); +} + +#[test] +fn command_send_metric_distribution_help() { + let _m = super::mock_envelopes_endpoint(); + integration::register_test("send_metric/send_metric-distribution-help.trycmd"); +} + +#[test] +fn command_send_metric_distribution_no_options() { + let _m = super::mock_envelopes_endpoint(); + integration::register_test("send_metric/send_metric-distribution-no-options.trycmd"); +} + +#[test] +fn command_send_metric_distribution_normalization() { + let _m = super::mock_envelopes_endpoint(); + integration::register_test("send_metric/send_metric-distribution-normalization.trycmd"); +} + +#[test] +fn command_send_metric_distribution_numerical_key_prefix() { + let _m = super::mock_envelopes_endpoint(); + integration::register_test("send_metric/send_metric-distribution-numerical-key-prefix.trycmd"); +} + +#[test] +fn command_send_metric_distribution_required_options_with_negative_value() { + let _m = super::mock_envelopes_endpoint(); + integration::register_test( + "send_metric/send_metric-distribution-required-options-with-negative-value.trycmd", + ); +} diff --git a/tests/integration/send_metric/gauge.rs b/tests/integration/send_metric/gauge.rs new file mode 100644 index 0000000000..3a2931e389 --- /dev/null +++ b/tests/integration/send_metric/gauge.rs @@ -0,0 +1,49 @@ +use crate::integration; + +#[test] +fn command_send_metric_gauge_all_options_long_with_float_value() { + let _m = super::mock_envelopes_endpoint(); + integration::register_test( + "send_metric/send_metric-gauge-all-options-long-with-float-value.trycmd", + ); +} + +#[test] +fn command_send_metric_gauge_all_options_short_with_int_value() { + let _m = super::mock_envelopes_endpoint(); + integration::register_test( + "send_metric/send_metric-gauge-all-options-short-with-int-value.trycmd", + ); +} + +#[test] +fn command_send_metric_gauge_help() { + let _m = super::mock_envelopes_endpoint(); + integration::register_test("send_metric/send_metric-gauge-help.trycmd"); +} + +#[test] +fn command_send_metric_gauge_no_options() { + let _m = super::mock_envelopes_endpoint(); + integration::register_test("send_metric/send_metric-gauge-no-options.trycmd"); +} + +#[test] +fn command_send_metric_gauge_normalization() { + let _m = super::mock_envelopes_endpoint(); + integration::register_test("send_metric/send_metric-gauge-normalization.trycmd"); +} + +#[test] +fn command_send_metric_gauge_numerical_key_prefix() { + let _m = super::mock_envelopes_endpoint(); + integration::register_test("send_metric/send_metric-gauge-numerical-key-prefix.trycmd"); +} + +#[test] +fn command_send_metric_gauge_required_options_with_negative_value() { + let _m = super::mock_envelopes_endpoint(); + integration::register_test( + "send_metric/send_metric-gauge-required-options-with-negative-value.trycmd", + ); +} diff --git a/tests/integration/send_metric/increment.rs b/tests/integration/send_metric/increment.rs new file mode 100644 index 0000000000..fffcba0e1d --- /dev/null +++ b/tests/integration/send_metric/increment.rs @@ -0,0 +1,69 @@ +use crate::integration::{self, EndpointOptions}; +use trycmd::TestCases; + +#[test] +fn command_send_metric_increment_all_options_long_with_float_value() { + let _m = super::mock_envelopes_endpoint(); + integration::register_test( + "send_metric/send_metric-increment-all-options-long-with-float-value.trycmd", + ); +} + +#[test] +fn command_send_metric_increment_all_options_short_with_int_value() { + let _m = super::mock_envelopes_endpoint(); + integration::register_test( + "send_metric/send_metric-increment-all-options-short-with-int-value.trycmd", + ); +} + +#[test] +fn command_send_metric_increment_help() { + let _m = super::mock_envelopes_endpoint(); + integration::register_test("send_metric/send_metric-increment-help.trycmd"); +} + +#[test] +fn command_send_metric_increment_no_dsn() { + let _m = super::mock_envelopes_endpoint(); + TestCases::new() + .case("tests/integration/_cases/send_metric/send_metric-increment-no-dsn.trycmd"); +} + +#[test] +fn command_send_metric_increment_no_options() { + let _m = super::mock_envelopes_endpoint(); + integration::register_test("send_metric/send_metric-increment-no-options.trycmd"); +} + +#[test] +fn command_send_metric_increment_normalization_with_negative_value() { + let _m = super::mock_envelopes_endpoint(); + integration::register_test( + "send_metric/send_metric-increment-normalization-with-negative-value.trycmd", + ); +} + +#[test] +fn command_send_metric_increment_numerical_key_prefix() { + let _m = super::mock_envelopes_endpoint(); + integration::register_test("send_metric/send_metric-increment-numerical-key-prefix.trycmd"); +} + +#[test] +fn command_send_metric_increment_required_options() { + let _m = super::mock_envelopes_endpoint(); + integration::register_test("send_metric/send_metric-increment-required-options.trycmd"); +} + +#[test] +fn command_send_metric_increment_tag_no_colon() { + let _m = super::mock_envelopes_endpoint(); + integration::register_test("send_metric/send_metric-increment-tag-no-colon.trycmd"); +} + +#[test] +fn command_send_metric_increment_unsuccessful_api_call() { + let _m = integration::mock_endpoint(EndpointOptions::new("POST", "/api/1337/envelope/", 500)); + integration::register_test("send_metric/send_metric-increment-unsuccessful-api-call.trycmd"); +} diff --git a/tests/integration/send_metric/mod.rs b/tests/integration/send_metric/mod.rs new file mode 100644 index 0000000000..7d0b24ff06 --- /dev/null +++ b/tests/integration/send_metric/mod.rs @@ -0,0 +1,34 @@ +use super::EndpointOptions; +use crate::integration; +use mockito::{Matcher, Mock}; + +mod distribution; +mod gauge; +mod increment; +mod set; + +fn mock_envelopes_endpoint() -> Mock { + let expected_auth_header = Matcher::Regex( + r#"^Sentry sentry_key=test, sentry_version=7, sentry_timestamp=\d{10}(\.[0-9]+)?, sentry_client=sentry-cli/.*"# + .to_string(), + ); + integration::mock_endpoint( + EndpointOptions::new("POST", "/api/1337/envelope/", 200) + .with_header_matcher("X-Sentry-Auth", expected_auth_header), + ) +} + +#[test] +fn command_send_metric_help() { + integration::register_test("send_metric/send_metric-help.trycmd"); +} + +#[test] +fn command_send_metric_no_subcommand() { + integration::register_test("send_metric/send_metric-no-subcommand.trycmd"); +} + +#[test] +fn command_send_metric_global_options() { + integration::register_test("send_metric/send_metric-global-options.trycmd"); +} diff --git a/tests/integration/send_metric/set.rs b/tests/integration/send_metric/set.rs new file mode 100644 index 0000000000..3654bbb5ba --- /dev/null +++ b/tests/integration/send_metric/set.rs @@ -0,0 +1,50 @@ +use super::mock_envelopes_endpoint; +use crate::integration::register_test; + +#[test] +fn command_send_metric_set_all_options_long_with_alphabetic_value() { + let _m = mock_envelopes_endpoint(); + register_test("send_metric/send_metric-set-all-options-long-with-alphabetic-value.trycmd"); +} + +#[test] +fn command_send_metric_set_all_options_short_with_int_value() { + let _m = mock_envelopes_endpoint(); + register_test("send_metric/send_metric-set-all-options-short-with-int-value.trycmd"); +} + +#[test] +fn command_send_metric_set_float_value() { + let _m = mock_envelopes_endpoint(); + register_test("send_metric/send_metric-set-float-value.trycmd"); +} + +#[test] +fn command_send_metric_set_help() { + let _m = mock_envelopes_endpoint(); + register_test("send_metric/send_metric-set-help.trycmd"); +} + +#[test] +fn command_send_metric_set_no_options() { + let _m = mock_envelopes_endpoint(); + register_test("send_metric/send_metric-set-no-options.trycmd"); +} + +#[test] +fn command_send_metric_set_normalization() { + let _m = mock_envelopes_endpoint(); + register_test("send_metric/send_metric-set-normalization.trycmd"); +} + +#[test] +fn command_send_metric_set_numerical_key_prefix() { + let _m = mock_envelopes_endpoint(); + register_test("send_metric/send_metric-set-numerical-key-prefix.trycmd"); +} + +#[test] +fn command_send_metric_set_required_options_with_negative_value() { + let _m = mock_envelopes_endpoint(); + register_test("send_metric/send_metric-set-required-options-with-negative-value.trycmd"); +}