From 4815045aa6ffe01619779094374c493e539da064 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 --- CONTRIBUTING.md | 10 ++ Cargo.lock | 107 ++++++++--- Cargo.toml | 5 +- src/api/envelopes_api.rs | 45 +++++ src/api/errors/api_error.rs | 7 + src/api/mod.rs | 7 + src/commands/derive_parser.rs | 36 ++++ src/commands/mod.rs | 3 + src/commands/send_metric/common_args.rs | 51 ++++++ src/commands/send_metric/distribution.rs | 16 ++ src/commands/send_metric/gauge.rs | 16 ++ src/commands/send_metric/increment.rs | 26 +++ src/commands/send_metric/mod.rs | 56 ++++++ src/commands/send_metric/set.rs | 61 +++++++ src/utils/metrics/mod.rs | 7 + src/utils/metrics/normalized_key.rs | 41 +++++ src/utils/metrics/normalized_payload.rs | 74 ++++++++ src/utils/metrics/normalized_tags.rs | 166 ++++++++++++++++++ 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 | 23 +++ ...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 | 23 +++ .../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 | 45 +++++ ...t-all-options-long-with-float-value.trycmd | 10 ++ ...nt-all-options-short-with-int-value.trycmd | 10 ++ .../send_metric-increment-help.trycmd | 23 +++ .../send_metric-increment-no-dsn.trycmd | 12 ++ .../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-bad-value-type.trycmd | 8 + .../send_metric-set-empty-value-type.trycmd | 8 + .../send_metric-set-float-value-as-int.trycmd | 10 ++ ...nd_metric-set-float-value-as-string.trycmd | 10 ++ .../send_metric/send_metric-set-help.trycmd | 25 +++ .../send_metric-set-no-options.trycmd | 13 ++ .../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 | 23 ++- 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 | 68 +++++++ 69 files changed, 1598 insertions(+), 30 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_key.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-bad-value-type.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-set-empty-value-type.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-set-float-value-as-int.trycmd create mode 100644 tests/integration/_cases/send_metric/send_metric-set-float-value-as-string.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/CONTRIBUTING.md b/CONTRIBUTING.md index 493a960be8..881a0249e8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,13 @@ +# Adding new commands with Derive API +It is recommended to use clap's [Derive API](https://docs.rs/clap/latest/clap/_derive/index.html) +as opposed to the [Builder API](https://docs.rs/clap/latest/clap/_tutorial/index.html). +The reason for this is that the Derive API makes it: +- Easier to read, write, and modify. +- Easier to keep the argument declaration and reading of argument in sync. +- Easier to reuse shared arguments. + +An existing example of how to use the Derive API in `sentry-cli` is the `send-metric`command. + # Integration Tests Integration tests are written using `trycmd` crate. Consult the docs in case you need to understand how it works https://docs.rs/trycmd/latest/trycmd/. diff --git a/Cargo.lock b/Cargo.lock index c7b26a6574..67bb6af4b1 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", @@ -2508,9 +2548,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 +2562,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 +2575,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", @@ -3355,6 +3395,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..04324d5534 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" diff --git a/src/api/envelopes_api.rs b/src/api/envelopes_api.rs new file mode 100644 index 0000000000..3b734834bb --- /dev/null +++ b/src/api/envelopes_api.rs @@ -0,0 +1,45 @@ +use super::{ + errors::{ApiErrorKind, ApiResult}, + Api, ApiResponse, Method, +}; +use crate::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(); + match api.config.get_dsn() { + Ok(dsn) => Ok(EnvelopesApi { api, dsn }), + Err(_) => 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 = Vec::new(); + envelope + .to_writer(&mut body) + .map_err(|_| ApiErrorKind::CannotSerializeEnvelope)?; + 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..77b801ea1c 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,11 @@ pub(in crate::api) enum ApiErrorKind { "Auth token is required for this request. Please run `sentry-cli login` and try again!" )] AuthMissing, + #[error( + "DSN not found. To configure your DSN, export the environment variable SENTRY_DSN or \ + add it to your ~/.sentryclirc config as follows:\n[auth]\ndsn = " + )] + 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..031bdafa60 --- /dev/null +++ b/src/commands/send_metric/common_args.rs @@ -0,0 +1,51 @@ +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")] + pub(super) value: f64, +} + +/// Common arguments for all send-metric subcommands. +#[derive(Args)] +pub struct CommonMetricArgs { + #[arg(short, long, visible_alias = "name", visible_short_alias = 'n')] + #[arg(help = "Metric key/name")] + pub key: MetricKey, + + #[arg(short, long, help = "Metric unit")] + pub unit: Option, + + #[arg(short, long, value_delimiter=',', value_name = "KEY:VALUE", num_args = 1..)] + #[arg(value_parser=value_parsers::kv_parser, help = "Metric tags as key:value pairs")] + pub tags: Vec<(String, String)>, +} + +#[derive(Clone)] +pub struct MetricKey(pub String); + +impl FromStr for MetricKey { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + if s.chars() + .next() + .ok_or_else(|| anyhow!(format!("metric name cannot be empty")))? + .is_ascii_alphabetic() + { + Ok(MetricKey(s.to_string())) + } else { + Err(anyhow!(format!( + "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..a29f6a50fc --- /dev/null +++ b/src/commands/send_metric/increment.rs @@ -0,0 +1,26 @@ +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", 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..e9cd9020dc --- /dev/null +++ b/src/commands/send_metric/mod.rs @@ -0,0 +1,56 @@ +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.")] +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..ad50d72050 --- /dev/null +++ b/src/commands/send_metric/set.rs @@ -0,0 +1,61 @@ +use std::str::FromStr; + +use super::common_args::CommonMetricArgs; +use crate::{ + api::envelopes_api::EnvelopesApi, + utils::metrics::{ + normalized_payload::NormalizedPayload, types::MetricType, values::MetricValue, + }, +}; +use anyhow::{anyhow, Context, Result}; +use clap::{command, Args}; +use sentry::protocol::EnvelopeItem; + +#[derive(Args)] +pub(super) struct SetMetricArgs { + #[command(flatten)] + common: CommonMetricArgs, + + #[arg(short, long, help = "Metric value")] + value: String, + + #[arg(long, visible_alias = "vt", ignore_case = true)] + #[arg(help = "Metric value type: `string` or `int`. Strings are hashed before being sent.")] + value_type: ValueType, +} + +#[derive(Clone)] +enum ValueType { + String, + Int, +} + +impl FromStr for ValueType { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + Err(anyhow!("value type cannot be empty")) + } else if "integer".starts_with(&s.to_ascii_lowercase()) { + Ok(ValueType::Int) + } else if "string".starts_with(&s.to_ascii_lowercase()) { + Ok(ValueType::String) + } else { + Err(anyhow!("value type must be either `string` or `int`")) + } + } +} + +pub(super) fn execute(args: SetMetricArgs) -> Result<()> { + let value = MetricValue::Int(match args.value_type { + ValueType::String => crc32fast::hash(args.value.as_bytes()) as i64, + ValueType::Int => args + .value + .parse::() + .context("the provided metric value is not parsable as an integer")?, + }); + println!("{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..c991c24c69 --- /dev/null +++ b/src/utils/metrics/mod.rs @@ -0,0 +1,7 @@ +pub mod normalized_payload; +pub mod types; +pub mod values; + +mod normalized_key; +mod normalized_tags; +mod normalized_unit; diff --git a/src/utils/metrics/normalized_key.rs b/src/utils/metrics/normalized_key.rs new file mode 100644 index 0000000000..02a4a65f2e --- /dev/null +++ b/src/utils/metrics/normalized_key.rs @@ -0,0 +1,41 @@ +use crate::commands::send_metric::common_args::MetricKey; +use regex::Regex; +use std::borrow::Cow; + +pub(super) struct NormalizedKey<'a> { + key: Cow<'a, str>, +} + +impl<'a> From<&'a MetricKey> for NormalizedKey<'a> { + fn from(key: &'a MetricKey) -> Self { + Self { + key: Regex::new(r"[^a-zA-Z0-9_\-.]") + .expect("Regex should compile") + .replace_all(&key.0, "_"), + } + } +} + +impl std::fmt::Display for NormalizedKey<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.key) + } +} + +#[cfg(test)] +mod test { + use crate::{ + commands::send_metric::common_args::MetricKey, + utils::metrics::normalized_key::NormalizedKey, + }; + + #[test] + fn test_from() { + let expected = "aA1_-.____________"; + + let actual = + NormalizedKey::from(&MetricKey("aA1_-./+รถ{๐Ÿ˜€\n\t\r\\| ,".to_string())).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..c3617ef764 --- /dev/null +++ b/src/utils/metrics/normalized_payload.rs @@ -0,0 +1,74 @@ +use super::{ + normalized_key::NormalizedKey, 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> { + key: NormalizedKey<'a>, + value: MetricValue, + metric_type: MetricType, + unit: NormalizedUnit<'a>, + tags: NormalizedTags<'a>, +} + +impl<'a> NormalizedPayload<'a> { + pub fn from_cli_args( + common_args: &'a CommonMetricArgs, + value: MetricValue, + metric_type: MetricType, + ) -> Self { + Self { + key: NormalizedKey::from(&common_args.key), + 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.key, self.unit, self.value, self.metric_type, self.tags, timestamp + ); + Ok(data.into_bytes()) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + commands::send_metric::common_args::{CommonMetricArgs, MetricKey}, + 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 { + key: MetricKey("nรถme".to_string()), + 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::Int(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..596d968770 --- /dev/null +++ b/src/utils/metrics/normalized_tags.rs @@ -0,0 +1,166 @@ +use crate::{config::Config, utils::releases}; +use itertools::Itertools; +use regex::Regex; +use std::{borrow::Cow, cmp::min, collections::HashMap}; + +pub(super) struct NormalizedTags<'a> { + tags: HashMap, String>, +} + +impl<'a> From<&'a [(String, String)]> for NormalizedTags<'a> { + fn from(tags: &'a [(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) -> Cow { + Regex::new(r"[^a-zA-Z0-9_\-./]") + .expect("Tag normalization regex should compile") + .replace_all(&key[..min(key.len(), 32)], "") + } + + fn normalize_value(value: &str) -> String { + value[..min(value.len(), 200)] + .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(Cow::Borrowed("release")) + .or_insert(Self::normalize_value(&release)); + } + self.tags + .entry(Cow::Borrowed("environment")) + .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 = "abcdefghijklmnopqrstuvwxyzabcdef:abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqr"; + + let actual = NormalizedTags::from(vec![ + ("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz".to_string(), + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz".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..ac914e295b --- /dev/null +++ b/src/utils/metrics/values.rs @@ -0,0 +1,16 @@ +use core::fmt; +use std::fmt::{Display, Formatter}; + +pub enum MetricValue { + Int(i64), + Float(f64), +} + +impl Display for MetricValue { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + MetricValue::Int(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..be7d85a263 --- /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 --key 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..d473d01733 --- /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 -k 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..325ac4dd9d --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-distribution-help.trycmd @@ -0,0 +1,23 @@ +``` +$ sentry-cli send-metric distribution --help +? success +Update a distribution metric with the provided value + +Usage: sentry-cli[EXE] send-metric distribution [OPTIONS] --key --value + +Options: + -k, --key Metric key/name [aliases: name] [short aliases: n] + --header Custom headers that should be attached to all requests + in key:value format. + -u, --unit Metric unit + --auth-token Use the given Sentry auth token. + -t, --tags ... Metric tags as key:value pairs + -v, --value Metric value + --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..806a30a1b3 --- /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: + --key + --value + +Usage: sentry-cli[EXE] send-metric distribution --key --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..e8cd9d2299 --- /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 -k "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..1df4a9761d --- /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 -k 1mymetric +? failed +error: invalid value '1mymetric' for '--key ': 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..1d2273c727 --- /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 -k 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..239dc54407 --- /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 --key 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..f1c53cdfe4 --- /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 -k 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..2e281d5764 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-gauge-help.trycmd @@ -0,0 +1,23 @@ +``` +$ sentry-cli send-metric gauge --help +? success +Update a gauge metric with the provided value + +Usage: sentry-cli[EXE] send-metric gauge [OPTIONS] --key --value + +Options: + -k, --key Metric key/name [aliases: name] [short aliases: n] + --header Custom headers that should be attached to all requests + in key:value format. + -u, --unit Metric unit + --auth-token Use the given Sentry auth token. + -t, --tags ... Metric tags as key:value pairs + -v, --value Metric value + --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-gauge-no-options.trycmd b/tests/integration/_cases/send_metric/send_metric-gauge-no-options.trycmd new file mode 100644 index 0000000000..28defdedae --- /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: + --key + --value + +Usage: sentry-cli[EXE] send-metric gauge --key --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..fb168b9ee8 --- /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 -k "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..8a42a07b64 --- /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 -k 1mymetric +? failed +error: invalid value '1mymetric' for '--key ': 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..fe9d80e3f3 --- /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 -k 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..d6901218ad --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-help.trycmd @@ -0,0 +1,45 @@ +``` +$ 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. + +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..b42ce7c5d9 --- /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 --key 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..e826eab540 --- /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 -k 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..bd44858c2a --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-increment-help.trycmd @@ -0,0 +1,23 @@ +``` +$ sentry-cli send-metric increment --help +? success +Increment a counter metric + +Usage: sentry-cli[EXE] send-metric increment [OPTIONS] --key + +Options: + -k, --key Metric key/name [aliases: name] [short aliases: n] + --header Custom headers that should be attached to all requests + in key:value format. + -u, --unit Metric unit + --auth-token Use the given Sentry auth token. + -t, --tags ... Metric tags as key:value pairs + -v, --value Metric value [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..4a8205508b --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-increment-no-dsn.trycmd @@ -0,0 +1,12 @@ +``` +$ sentry-cli send-metric increment -k testmetric +? failed +... +error: DSN not found. To configure your DSN, export the environment variable SENTRY_DSN or add it to your ~/.sentryclirc config as follows: +[auth] +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..e9bf9c8c9e --- /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: + --key + +Usage: sentry-cli[EXE] send-metric increment --key + +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..438ee83f84 --- /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 -k "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..86c8f48d29 --- /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 -k 1mymetric +? failed +error: invalid value '1mymetric' for '--key ': 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..244770e6d5 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-increment-required-options.trycmd @@ -0,0 +1,10 @@ +``` +$ sentry-cli send-metric increment -k 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..57ee990517 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-increment-tag-no-colon.trycmd @@ -0,0 +1,8 @@ +``` +$ sentry-cli send-metric increment -k 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..3271d1c526 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-increment-unsuccessful-api-call.trycmd @@ -0,0 +1,10 @@ +``` +$ sentry-cli send-metric increment -k 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..52c5f07e63 --- /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 --key testMetric --value abc --unit mb --tags a:b --vt string +? 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..d4ed41c8e5 --- /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 -k testMetric -v 2 -u mb -t a:b,environment:testEnvironment,release:testRelease --vt int +? success +... +{} +{"type":"statsd","length":[..]} +testMetric@mb:2|s|#a:b,environment:testEnvironment,release:testRelease|T[..] +... + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-set-bad-value-type.trycmd b/tests/integration/_cases/send_metric/send_metric-set-bad-value-type.trycmd new file mode 100644 index 0000000000..8cfa629b64 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-set-bad-value-type.trycmd @@ -0,0 +1,8 @@ +``` +$ sentry-cli --log-level debug send-metric set -k testMetric -v 2 --vt q +? failed +error: invalid value 'q' for '--value-type ': value type must be either `string` or `int` + +For more information, try '--help'. + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-set-empty-value-type.trycmd b/tests/integration/_cases/send_metric/send_metric-set-empty-value-type.trycmd new file mode 100644 index 0000000000..72979885d1 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-set-empty-value-type.trycmd @@ -0,0 +1,8 @@ +``` +$ sentry-cli --log-level debug send-metric set -k testMetric -v 2 --vt="" +? failed +error: invalid value '' for '--value-type ': value type cannot be empty + +For more information, try '--help'. + +``` diff --git a/tests/integration/_cases/send_metric/send_metric-set-float-value-as-int.trycmd b/tests/integration/_cases/send_metric/send_metric-set-float-value-as-int.trycmd new file mode 100644 index 0000000000..34955d7e16 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-set-float-value-as-int.trycmd @@ -0,0 +1,10 @@ +``` +$ sentry-cli send-metric set -k testmetric -v 1.9 --vt int +? failed +error: the provided metric value is not parsable as an integer + caused by: invalid digit found in string + +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-set-float-value-as-string.trycmd b/tests/integration/_cases/send_metric/send_metric-set-float-value-as-string.trycmd new file mode 100644 index 0000000000..d8d1a3eba0 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-set-float-value-as-string.trycmd @@ -0,0 +1,10 @@ +``` +$ sentry-cli send-metric set -k testmetric -v 1.9 --log-level debug --vt string +? 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..aa157892cd --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-set-help.trycmd @@ -0,0 +1,25 @@ +``` +$ sentry-cli send-metric set --help +? success +Update a set metric with the provided value + +Usage: sentry-cli[EXE] send-metric set [OPTIONS] --key --value --value-type + +Options: + -k, --key Metric key/name [aliases: name] [short aliases: n] + --header Custom headers that should be attached to all requests + in key:value format. + -u, --unit Metric unit + --auth-token Use the given Sentry auth token. + -t, --tags ... Metric tags as key:value pairs + -v, --value Metric value + --log-level Set the log output verbosity. [possible values: trace, debug, info, + warn, error] + --value-type Metric value type: `string` or `int`. Strings are hashed before + being sent. [aliases: vt] + --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..b41c4696d0 --- /dev/null +++ b/tests/integration/_cases/send_metric/send_metric-set-no-options.trycmd @@ -0,0 +1,13 @@ +``` +$ sentry-cli send-metric set +? failed +error: the following required arguments were not provided: + --key + --value + --value-type + +Usage: sentry-cli[EXE] send-metric set --key --value --value-type + +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..224e52a989 --- /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 -k "aA1_-./+รถ{๐Ÿ˜€ |\\," -u "aA1_-./+รถ{๐Ÿ˜€ |\\," -t "aA1_-./+รถ{๐Ÿ˜€ |\\:aA1_-./+รถ{๐Ÿ˜€ |\\" release:testRelease +:b +:+ a: :b : --vt i +? success +... +{} +{"type":"statsd","length":[..]} +aA1_-.__________@aA1_:1|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..f319a20145 --- /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 -k 1mymetric +? failed +error: invalid value '1mymetric' for '--key ': 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..d8e9bde94e --- /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 -k testMetric -v=-3 --log-level debug --value-type in +? success +... +{} +{"type":"statsd","length":[..]} +testMetric@none:-3|s|#environment:production,release:[..]|T[..] +... + +``` diff --git a/tests/integration/mod.rs b/tests/integration/mod.rs index 0387b6c869..9a371e56b5 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, 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_URL", mockito::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,10 +95,15 @@ impl EndpointOptions { self.matcher = Some(matcher); self } + + 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 { - let mut mock = mock(opts.method.as_str(), opts.endpoint.as_str()) + let mut mock = mockito::mock(opts.method.as_str(), opts.endpoint.as_str()) .with_status(opts.status) .with_header("content-type", "application/json"); @@ -110,6 +119,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() } @@ -185,7 +198,7 @@ pub fn mock_common_upload_endpoints( \"hashAlgorithm\": \"sha1\", \"accept\": [{}] }}", - server_url(), + mockito::server_url(), accept, ); 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..795ade7338 --- /dev/null +++ b/tests/integration/send_metric/set.rs @@ -0,0 +1,68 @@ +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_empty_value_type() { + let _m = mock_envelopes_endpoint(); + register_test("send_metric/send_metric-set-empty-value-type.trycmd"); +} + +#[test] +fn command_send_metric_set_bad_value_type() { + let _m = mock_envelopes_endpoint(); + register_test("send_metric/send_metric-set-bad-value-type.trycmd"); +} + +#[test] +fn command_send_metric_set_float_value_as_int() { + let _m = mock_envelopes_endpoint(); + register_test("send_metric/send_metric-set-float-value-as-int.trycmd"); +} + +#[test] +fn command_send_metric_set_float_value_as_string() { + let _m = mock_envelopes_endpoint(); + register_test("send_metric/send_metric-set-float-value-as-string.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"); +}