diff --git a/relay-event-normalization/src/eap/mod.rs b/relay-event-normalization/src/eap/mod.rs index d2405559eed..c223fa61ffb 100644 --- a/relay-event-normalization/src/eap/mod.rs +++ b/relay-event-normalization/src/eap/mod.rs @@ -9,8 +9,8 @@ use chrono::{DateTime, Utc}; use relay_common::time::UnixTimestamp; use relay_conventions::attributes::*; use relay_conventions::{AttributeInfo, WriteBehavior}; -use relay_event_schema::protocol::{AttributeType, Attributes, BrowserContext, Geo}; -use relay_protocol::{Annotated, ErrorKind, Meta, Remark, RemarkType, Value}; +use relay_event_schema::protocol::{Attribute, AttributeType, Attributes, BrowserContext, Geo}; +use relay_protocol::{Annotated, Error, ErrorKind, Meta, Remark, RemarkType, Value}; use relay_sampling::DynamicSamplingContext; use relay_spans::derive_op_for_v2_span; @@ -387,6 +387,35 @@ pub fn normalize_dsc( } } +/// Normalizes the client sample rate attribute to be in the range `(0, 1]`. +/// +/// This is only relevant for spans as other eap types re not sampled. +pub fn normalize_client_sample_rate(attributes: &mut Annotated) { + let Some(attributes) = attributes.value_mut() else { + return; + }; + + // This is fine if normalizations like this stay one-offs. If at some point we end up with more + // of these structural validations or normalizations based on attributes, they should be + // outsourced to conventions and enforced with a dedicated processor. + fn normalize_sample_rate(sr: &Annotated) -> Option> { + match sr.value()?.value.value.value()?.as_f64() { + Some(v) if v > 0.0 && v <= 1.0 => None, + // This is an invalid sample rate, either by type or value. + _ => Some(Annotated::from_error( + Error::expected("sample rate > 0.0, <= 1.0"), + None, + )), + } + } + + if let Some(sr) = attributes.0.get_mut(SENTRY__CLIENT_SAMPLE_RATE) + && let Some(new_sr) = normalize_sample_rate(sr) + { + *sr = new_sr; + } +} + /// Normalizes deprecated attributes according to `sentry-conventions`. /// /// Attributes with a status of `"normalize"` will be moved to their replacement name. @@ -2525,4 +2554,121 @@ mod tests { } "#); } + + #[test] + fn test_normalize_client_sample_rate_valid() { + let mut attributes = Annotated::from_json( + r#"{ + "sentry.client_sample_rate": { + "type": "double", + "value": 1.0 + } + }"#, + ) + .unwrap(); + + normalize_client_sample_rate(&mut attributes); + + assert_annotated_snapshot!(attributes, @r#" + { + "sentry.client_sample_rate": { + "type": "double", + "value": 1.0 + } + } + "#); + } + + #[test] + fn test_normalize_client_sample_rate_invalid_too_small() { + let mut attributes = { + let mut attrs = Attributes::new(); + attrs.insert(SENTRY__CLIENT_SAMPLE_RATE, 0.0); + Annotated::new(attrs) + }; + + normalize_client_sample_rate(&mut attributes); + + assert_annotated_snapshot!(attributes, @r#" + { + "sentry.client_sample_rate": null, + "_meta": { + "sentry.client_sample_rate": { + "": { + "err": [ + [ + "invalid_data", + { + "reason": "expected sample rate > 0.0, <= 1.0" + } + ] + ] + } + } + } + } + "#); + } + + #[test] + fn test_normalize_client_sample_rate_invalid_too_large() { + let mut attributes = { + let mut attrs = Attributes::new(); + attrs.insert(SENTRY__CLIENT_SAMPLE_RATE, 1.1); + Annotated::new(attrs) + }; + + normalize_client_sample_rate(&mut attributes); + + assert_annotated_snapshot!(attributes, @r#" + { + "sentry.client_sample_rate": null, + "_meta": { + "sentry.client_sample_rate": { + "": { + "err": [ + [ + "invalid_data", + { + "reason": "expected sample rate > 0.0, <= 1.0" + } + ] + ] + } + } + } + } + "#); + } + + #[test] + fn test_normalize_client_sample_rate_invalid_type() { + let mut attributes = { + let mut attrs = Attributes::new(); + attrs.insert(SENTRY__CLIENT_SAMPLE_RATE, "foobar"); + Annotated::new(attrs) + }; + + normalize_client_sample_rate(&mut attributes); + + assert_annotated_snapshot!(attributes, @r#" + { + "sentry.client_sample_rate": null, + "_meta": { + "sentry.client_sample_rate": { + "": { + "err": [ + [ + "invalid_data", + { + "reason": "expected sample rate > 0.0, <= 1.0" + } + ] + ] + } + } + } + } + "#); + } } diff --git a/relay-server/src/processing/spans/process.rs b/relay-server/src/processing/spans/process.rs index f3ad8a293d1..4c0cdb7e93b 100644 --- a/relay-server/src/processing/spans/process.rs +++ b/relay-server/src/processing/spans/process.rs @@ -232,6 +232,7 @@ fn normalize_span( relay_event_normalization::normalize_performance_score(span, performance_score); eap::normalize_attribute_values(&mut span.attributes, allowed_hosts); eap::write_legacy_attributes(&mut span.attributes); + eap::normalize_client_sample_rate(&mut span.attributes); }; // Set a max_bytes value on the root state if it's defined in the project config. diff --git a/relay-server/src/processing/spans/store.rs b/relay-server/src/processing/spans/store.rs index c58884cee28..3a26425462c 100644 --- a/relay-server/src/processing/spans/store.rs +++ b/relay-server/src/processing/spans/store.rs @@ -58,5 +58,8 @@ fn inject_server_sample_rate( }; let attributes = attributes.get_or_insert_with(Default::default); - attributes.insert("sentry.server_sample_rate", server_sample_rate.to_f64()); + attributes.insert( + "sentry.server_sample_rate", + server_sample_rate.to_f64().clamp(1e-9, 1.0), + ); }