Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(protocol): Support comparisons in rule conditions on strings #2721

Merged
merged 9 commits into from
Nov 16, 2023
29 changes: 28 additions & 1 deletion py/tests/test_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ def test_invalid_sampling_condition():
sentry_relay.validate_rule_condition(condition)


def test_validate_sampling_configuration():
def test_validate_legacy_sampling_configuration():
"""
Tests that a valid sampling rule configuration passes
"""
Expand Down Expand Up @@ -288,6 +288,33 @@ def test_validate_sampling_configuration():
sentry_relay.validate_sampling_configuration(config)


def test_validate_sampling_configuration():
"""
Tests that a valid sampling rule configuration passes
"""
config = """{
"version": 2,
"rules": [
{
"type": "trace",
"samplingValue": {
"type": "sampleRate",
"value": 0.9
},
"condition": {
"op": "eq",
"name": "event.release",
"value":["1.1.*"],
jan-auer marked this conversation as resolved.
Show resolved Hide resolved
"options": {"ignoreCase": true}
},
"id":2
}
]
}"""
# Should NOT throw
sentry_relay.validate_sampling_configuration(config)


def test_validate_project_config():
config = {"allowedDomains": ["*"], "trustedRelays": [], "piiConfig": None}
# Does not raise:
Expand Down
3 changes: 2 additions & 1 deletion relay-dynamic-config/src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ pub struct CustomMeasurementConfig {
/// - 3:
/// - Emit a `usage` metric and use it for rate limiting.
/// - Delay metrics extraction for indexed transactions.
const TRANSACTION_EXTRACT_VERSION: u16 = 3;
/// - 4: Adds support for `RuleConfigs` with string comparisons.
const TRANSACTION_EXTRACT_VERSION: u16 = 4;

/// Deprecated. Defines whether URL transactions should be considered low cardinality.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
Expand Down
12 changes: 8 additions & 4 deletions relay-dynamic-config/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ pub struct ProjectConfig {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub quotas: Vec<Quota>,
/// Configuration for sampling traces, if not present there will be no sampling.
#[serde(skip_serializing_if = "Option::is_none")]
pub dynamic_sampling: Option<SamplingConfig>,
#[serde(alias = "dynamicSampling", skip_serializing_if = "Option::is_none")]
pub sampling: Option<ErrorBoundary<SamplingConfig>>,
/// Configuration for measurements.
/// NOTE: do not access directly, use [`relay_event_normalization::DynamicMeasurementsConfig`].
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down Expand Up @@ -97,6 +97,10 @@ impl ProjectConfig {

metrics::convert_conditional_tagging(self);
defaults::add_span_metrics(self);

if let Some(ErrorBoundary::Ok(ref mut sampling_config)) = self.sampling {
sampling_config.normalize();
}
}
}

Expand All @@ -111,7 +115,7 @@ impl Default for ProjectConfig {
datascrubbing_settings: DataScrubbingConfig::default(),
event_retention: None,
quotas: Vec::new(),
dynamic_sampling: None,
sampling: None,
measurements: None,
breakdowns_v2: None,
performance_score: Default::default(),
Expand Down Expand Up @@ -150,7 +154,7 @@ pub struct LimitedProjectConfig {
#[serde(skip_serializing_if = "DataScrubbingConfig::is_disabled")]
pub datascrubbing_settings: DataScrubbingConfig,
#[serde(skip_serializing_if = "Option::is_none")]
pub dynamic_sampling: Option<SamplingConfig>,
pub sampling: Option<ErrorBoundary<SamplingConfig>>,
#[serde(skip_serializing_if = "SessionMetricsConfig::is_disabled")]
pub session_metrics: SessionMetricsConfig,
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down
17 changes: 10 additions & 7 deletions relay-protocol/src/condition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use relay_common::glob3::GlobPatterns;
use serde::{Deserialize, Serialize};
use serde_json::{Number, Value};
use serde_json::Value;

use crate::{Getter, Val};

Expand Down Expand Up @@ -106,12 +106,12 @@ macro_rules! impl_cmp_condition {
/// Path of the field that should match the value.
pub name: String,
/// The numeric value to check against.
pub value: Number,
pub value: Value,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now since this is a value, should we also add array support like Eq has?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't wanna add this since the semantics of a comparison with an array are questionable. For eq, we defined it as "one of", but for comparisons this seemed weird to me. Thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally found it already surprising for Eq, usually this would be a In operator. So for me then I assumed the value can always be an array and it turns into something like value <OPERATOR> ANY(value).

I agree though this is weird but we already have a precedent for the array. Though I think we just leave it as is and implement it when we need it (we can bump versions now, yay)

}

impl $struct_name {
/// Creates a new condition that comparison condition.
pub fn new(field: impl Into<String>, value: impl Into<Number>) -> Self {
pub fn new(field: impl Into<String>, value: impl Into<Value>) -> Self {
Self {
name: field.into(),
value: value.into(),
Expand All @@ -136,6 +136,8 @@ macro_rules! impl_cmp_condition {
a $operator b
} else if let (Some(a), Some(b)) = (value.as_f64(), self.value.as_f64()) {
a $operator b
} else if let (Some(a), Some(b)) = (value.as_str(), self.value.as_str()) {
a $operator b
} else {
false
}
Expand Down Expand Up @@ -527,7 +529,7 @@ impl RuleCondition {
///
/// let condition = RuleCondition::gt("obj.length", 10);
/// ```
pub fn gt(field: impl Into<String>, value: impl Into<Number>) -> Self {
pub fn gt(field: impl Into<String>, value: impl Into<Value>) -> Self {
Self::Gt(GtCondition::new(field, value))
}

Expand All @@ -540,7 +542,7 @@ impl RuleCondition {
///
/// let condition = RuleCondition::gte("obj.length", 10);
/// ```
pub fn gte(field: impl Into<String>, value: impl Into<Number>) -> Self {
pub fn gte(field: impl Into<String>, value: impl Into<Value>) -> Self {
Self::Gte(GteCondition::new(field, value))
}

Expand All @@ -553,7 +555,7 @@ impl RuleCondition {
///
/// let condition = RuleCondition::lt("obj.length", 10);
/// ```
pub fn lt(field: impl Into<String>, value: impl Into<Number>) -> Self {
pub fn lt(field: impl Into<String>, value: impl Into<Value>) -> Self {
Self::Lt(LtCondition::new(field, value))
}

Expand All @@ -566,7 +568,7 @@ impl RuleCondition {
///
/// let condition = RuleCondition::lte("obj.length", 10);
/// ```
pub fn lte(field: impl Into<String>, value: impl Into<Number>) -> Self {
pub fn lte(field: impl Into<String>, value: impl Into<Value>) -> Self {
Self::Lte(LteCondition::new(field, value))
}

Expand Down Expand Up @@ -934,6 +936,7 @@ mod tests {
& RuleCondition::eq_ignore_case("trace.user.segment", "vip"),
),
("match no conditions", RuleCondition::all()),
("string cmp", RuleCondition::gt("trace.transaction", "t")),
];

let dsc = mock_dsc();
Expand Down
82 changes: 56 additions & 26 deletions relay-sampling/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,36 @@ use serde::{Deserialize, Serialize};

use relay_protocol::RuleCondition;

/// Maximum supported version of dynamic sampling.
///
/// The version is an integer scalar, incremented by one on each new version:
/// - 1: Initial version that uses `rules_v2`.
/// - 2: Moves back to `rules` and adds support for `RuleConfigs` with string comparisons.
const SAMPLING_CONFIG_VERSION: u16 = 2;

/// Represents the dynamic sampling configuration available to a project.
///
/// Note: This comes from the organization data
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SamplingConfig {
/// The required version to run dynamic sampling.
///
/// Defaults to legacy version (`1`) when missing.
#[serde(default = "SamplingConfig::legacy_version")]
pub version: u16,

/// The ordered sampling rules for the project.
///
/// This field will remain here to serve only for old customer Relays to which we will
jan-auer marked this conversation as resolved.
Show resolved Hide resolved
/// forward the sampling config. The idea is that those Relays will get the old rules as
/// empty array, which will result in them not sampling and forwarding sampling decisions to
/// upstream Relays.
#[serde(default, skip_deserializing)]
#[serde(default)]
pub rules: Vec<SamplingRule>,

/// The ordered sampling rules v2 for the project.
jan-auer marked this conversation as resolved.
Show resolved Hide resolved
#[serde(default, skip_serializing)]
pub rules_v2: Vec<SamplingRule>,
Comment on lines +39 to 40
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In some enums we've renamed the option to deprecated and set a serde alias on it. Could that be helpful here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good idea, however it creates a difference between how Sentry serializes the schema and how it looks in Relay. I'm afraid that it causes more confusion when looking at the code.


/// Defines which population of items a dynamic sample rate applies to.
Expand All @@ -31,16 +45,42 @@ pub struct SamplingConfig {
}

impl SamplingConfig {
/// Creates an enabled configuration with empty defaults and the latest version.
pub fn new() -> Self {
Self::default()
}

/// Returns `true` if any of the rules in this configuration is unsupported.
pub fn unsupported(&self) -> bool {
!self.rules_v2.iter().all(SamplingRule::supported)
iker-barriocanal marked this conversation as resolved.
Show resolved Hide resolved
self.version > SAMPLING_CONFIG_VERSION || !self.rules.iter().all(SamplingRule::supported)
}

/// Filters the sampling rules by the given [`RuleType`].
pub fn filter_rules(&self, rule_type: RuleType) -> impl Iterator<Item = &SamplingRule> {
self.rules_v2
.iter()
.filter(move |rule| rule.ty == rule_type)
jan-auer marked this conversation as resolved.
Show resolved Hide resolved
self.rules.iter().filter(move |rule| rule.ty == rule_type)
}

/// Upgrades legacy sampling configs into the latest format.
pub fn normalize(&mut self) {
if self.version == Self::legacy_version() {
self.rules.append(&mut self.rules_v2);
self.version = SAMPLING_CONFIG_VERSION;
}
}

const fn legacy_version() -> u16 {
1
}
}

impl Default for SamplingConfig {
fn default() -> Self {
Self {
version: SAMPLING_CONFIG_VERSION,
rules: vec![],
rules_v2: vec![],
mode: SamplingMode::default(),
}
}
}

Expand Down Expand Up @@ -422,20 +462,9 @@ mod tests {
}

#[test]
fn test_sampling_config_with_rules_and_rules_v2_deserialization() {
fn test_legacy_deserialization() {
let serialized_rule = r#"{
"rules": [
{
"sampleRate": 0.5,
"type": "trace",
"active": true,
"condition": {
"op": "and",
"inner": []
},
"id": 1000
}
],
"rules": [],
Dav1dde marked this conversation as resolved.
Show resolved Hide resolved
"rulesV2": [
{
"samplingValue":{
Expand All @@ -453,36 +482,37 @@ mod tests {
],
"mode": "received"
}"#;
let config: SamplingConfig = serde_json::from_str(serialized_rule).unwrap();
let mut config: SamplingConfig = serde_json::from_str(serialized_rule).unwrap();
config.normalize();

// We want to make sure that we serialize an empty array of rule, irrespectively of the
// received payload.
assert!(config.rules.is_empty());
assert_eq!(config.version, SAMPLING_CONFIG_VERSION);
assert_eq!(
config.rules_v2[0].sampling_value,
config.rules[0].sampling_value,
SamplingValue::SampleRate { value: 0.5 }
);
assert!(config.rules_v2.is_empty());
}

#[test]
fn test_sampling_config_with_rules_and_rules_v2_serialization() {
let config = SamplingConfig {
rules: vec![],
rules_v2: vec![SamplingRule {
rules: vec![SamplingRule {
condition: RuleCondition::all(),
sampling_value: SamplingValue::Factor { value: 2.0 },
ty: RuleType::Transaction,
id: RuleId(1),
time_range: Default::default(),
decaying_fn: Default::default(),
}],
mode: SamplingMode::Received,
..SamplingConfig::new()
};

let serialized_config = serde_json::to_string_pretty(&config).unwrap();
let expected_serialized_config = r#"{
"rules": [],
"rulesV2": [
"version": 2,
"rules": [
{
"condition": {
"op": "and",
Expand Down
6 changes: 3 additions & 3 deletions relay-sampling/tests/fixtures/sampling_config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"rules": [],
"rulesV2": [
"version": 2,
"rules": [
{
"condition": {
"op": "and",
Expand Down Expand Up @@ -46,4 +46,4 @@
}
],
"mode": "total"
}
}
Loading
Loading