relay_dynamic_config/
project.rs

1use relay_auth::PublicKey;
2use relay_event_normalization::{
3    BreakdownsConfig, MeasurementsConfig, PerformanceScoreConfig, SpanDescriptionRule,
4    TransactionNameRule,
5};
6use relay_filter::ProjectFiltersConfig;
7use relay_pii::{DataScrubbingConfig, PiiConfig};
8use relay_quotas::Quota;
9use relay_sampling::SamplingConfig;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13use crate::error_boundary::ErrorBoundary;
14use crate::feature::FeatureSet;
15use crate::metrics::{
16    self, MetricExtractionConfig, Metrics, SessionMetricsConfig, TaggingRule,
17    TransactionMetricsConfig,
18};
19use crate::trusted_relay::TrustedRelayConfig;
20use crate::{GRADUATED_FEATURE_FLAGS, defaults};
21
22/// Dynamic, per-DSN configuration passed down from Sentry.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(default, rename_all = "camelCase")]
25pub struct ProjectConfig {
26    /// URLs that are permitted for cross original JavaScript requests.
27    pub allowed_domains: Vec<String>,
28    /// List of relay public keys that are permitted to access this project.
29    pub trusted_relays: Vec<PublicKey>,
30    /// Configuration for trusted Relay behaviour.
31    #[serde(skip_serializing_if = "TrustedRelayConfig::is_empty")]
32    pub trusted_relay_settings: TrustedRelayConfig,
33    /// Configuration for PII stripping.
34    pub pii_config: Option<PiiConfig>,
35    /// The grouping configuration.
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub grouping_config: Option<Value>,
38    /// Configuration for filter rules.
39    #[serde(skip_serializing_if = "ProjectFiltersConfig::is_empty")]
40    pub filter_settings: ProjectFiltersConfig,
41    /// Configuration for data scrubbers.
42    #[serde(skip_serializing_if = "DataScrubbingConfig::is_disabled")]
43    pub datascrubbing_settings: DataScrubbingConfig,
44    /// Maximum event retention for the organization.
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub event_retention: Option<u16>,
47    /// Maximum sampled event retention for the organization.
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub downsampled_event_retention: Option<u16>,
50    /// Usage quotas for this project.
51    #[serde(skip_serializing_if = "Vec::is_empty")]
52    pub quotas: Vec<Quota>,
53    /// Configuration for sampling traces, if not present there will be no sampling.
54    #[serde(alias = "dynamicSampling", skip_serializing_if = "Option::is_none")]
55    pub sampling: Option<ErrorBoundary<SamplingConfig>>,
56    /// Configuration for measurements.
57    /// NOTE: do not access directly, use [`relay_event_normalization::CombinedMeasurementsConfig`].
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub measurements: Option<MeasurementsConfig>,
60    /// Configuration for operation breakdown. Will be emitted only if present.
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub breakdowns_v2: Option<BreakdownsConfig>,
63    /// Configuration for performance score calculations. Will be emitted only if present.
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub performance_score: Option<PerformanceScoreConfig>,
66    /// Configuration for extracting metrics from sessions.
67    #[serde(skip_serializing_if = "SessionMetricsConfig::is_disabled")]
68    pub session_metrics: SessionMetricsConfig,
69    /// Configuration for extracting metrics from transaction events.
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub transaction_metrics: Option<ErrorBoundary<TransactionMetricsConfig>>,
72    /// Configuration for generic metrics extraction from all data categories.
73    #[serde(default, skip_serializing_if = "skip_metrics_extraction")]
74    pub metric_extraction: ErrorBoundary<MetricExtractionConfig>,
75    /// Rules for applying metrics tags depending on the event's content.
76    #[serde(skip_serializing_if = "Vec::is_empty")]
77    pub metric_conditional_tagging: Vec<TaggingRule>,
78    /// Exposable features enabled for this project.
79    #[serde(skip_serializing_if = "FeatureSet::is_empty")]
80    pub features: FeatureSet,
81    /// Transaction renaming rules.
82    #[serde(skip_serializing_if = "Vec::is_empty")]
83    pub tx_name_rules: Vec<TransactionNameRule>,
84    /// Whether or not a project is ready to mark all URL transactions as "sanitized".
85    #[serde(skip_serializing_if = "is_false")]
86    pub tx_name_ready: bool,
87    /// Span description renaming rules.
88    ///
89    /// These are currently not used by Relay, and only here to be forwarded to old
90    /// relays that might still need them.
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub span_description_rules: Option<Vec<SpanDescriptionRule>>,
93    /// Configuration for metrics.
94    #[serde(default, skip_serializing_if = "skip_metrics")]
95    pub metrics: ErrorBoundary<Metrics>,
96}
97
98impl ProjectConfig {
99    /// Validates fields in this project config and removes values that are partially invalid.
100    pub fn sanitize(&mut self) {
101        self.quotas.retain(Quota::is_valid);
102
103        metrics::convert_conditional_tagging(self);
104        defaults::add_span_metrics(self);
105
106        if let Some(ErrorBoundary::Ok(ref mut sampling_config)) = self.sampling {
107            sampling_config.normalize();
108        }
109
110        for flag in GRADUATED_FEATURE_FLAGS {
111            self.features.0.insert(*flag);
112        }
113
114        // Check if indexed and non-indexed are double-counting towards the same ID.
115        // This is probably not intended behavior.
116        for quota in &self.quotas {
117            if let Some(id) = &quota.id {
118                for category in &quota.categories {
119                    if let Some(indexed) = category.index_category()
120                        && quota.categories.contains(&indexed)
121                    {
122                        relay_log::error!(
123                            tags.id = id,
124                            "Categories {category} and {indexed} share the same quota ID. This will double-count items.",
125                        );
126                    }
127                }
128            }
129        }
130    }
131}
132
133impl Default for ProjectConfig {
134    fn default() -> Self {
135        ProjectConfig {
136            allowed_domains: vec!["*".to_owned()],
137            trusted_relays: vec![],
138            trusted_relay_settings: TrustedRelayConfig::default(),
139            pii_config: None,
140            grouping_config: None,
141            filter_settings: ProjectFiltersConfig::default(),
142            datascrubbing_settings: DataScrubbingConfig::default(),
143            event_retention: None,
144            downsampled_event_retention: None,
145            quotas: Vec::new(),
146            sampling: None,
147            measurements: None,
148            breakdowns_v2: None,
149            performance_score: Default::default(),
150            session_metrics: SessionMetricsConfig::default(),
151            transaction_metrics: None,
152            metric_extraction: Default::default(),
153            metric_conditional_tagging: Vec::new(),
154            features: Default::default(),
155            tx_name_rules: Vec::new(),
156            tx_name_ready: false,
157            span_description_rules: None,
158            metrics: Default::default(),
159        }
160    }
161}
162
163fn skip_metrics_extraction(boundary: &ErrorBoundary<MetricExtractionConfig>) -> bool {
164    match boundary {
165        ErrorBoundary::Err(_) => true,
166        ErrorBoundary::Ok(config) => !config.is_enabled(),
167    }
168}
169
170fn skip_metrics(boundary: &ErrorBoundary<Metrics>) -> bool {
171    match boundary {
172        ErrorBoundary::Err(_) => true,
173        ErrorBoundary::Ok(metrics) => metrics.is_empty(),
174    }
175}
176
177/// Subset of [`ProjectConfig`] that is passed to external Relays.
178///
179/// For documentation of the fields, see [`ProjectConfig`].
180#[allow(missing_docs)]
181#[derive(Debug, Serialize)]
182#[serde(rename_all = "camelCase", remote = "ProjectConfig")]
183pub struct LimitedProjectConfig {
184    pub allowed_domains: Vec<String>,
185    pub trusted_relays: Vec<PublicKey>,
186    pub pii_config: Option<PiiConfig>,
187    #[serde(skip_serializing_if = "ProjectFiltersConfig::is_empty")]
188    pub filter_settings: ProjectFiltersConfig,
189    #[serde(skip_serializing_if = "DataScrubbingConfig::is_disabled")]
190    pub datascrubbing_settings: DataScrubbingConfig,
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub sampling: Option<ErrorBoundary<SamplingConfig>>,
193    #[serde(skip_serializing_if = "SessionMetricsConfig::is_disabled")]
194    pub session_metrics: SessionMetricsConfig,
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub transaction_metrics: Option<ErrorBoundary<TransactionMetricsConfig>>,
197    #[serde(default, skip_serializing_if = "skip_metrics_extraction")]
198    pub metric_extraction: ErrorBoundary<MetricExtractionConfig>,
199    #[serde(skip_serializing_if = "Vec::is_empty")]
200    pub metric_conditional_tagging: Vec<TaggingRule>,
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub measurements: Option<MeasurementsConfig>,
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub breakdowns_v2: Option<BreakdownsConfig>,
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub performance_score: Option<PerformanceScoreConfig>,
207    #[serde(skip_serializing_if = "FeatureSet::is_empty")]
208    pub features: FeatureSet,
209    #[serde(skip_serializing_if = "Vec::is_empty")]
210    pub tx_name_rules: Vec<TransactionNameRule>,
211    /// Whether or not a project is ready to mark all URL transactions as "sanitized".
212    #[serde(skip_serializing_if = "is_false")]
213    pub tx_name_ready: bool,
214    /// Span description renaming rules.
215    ///
216    /// These are currently not used by Relay, and only here to be forwarded to old
217    /// relays that might still need them.
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub span_description_rules: Option<Vec<SpanDescriptionRule>>,
220}
221
222fn is_false(value: &bool) -> bool {
223    !*value
224}
225
226#[cfg(test)]
227mod tests {
228    use crate::Feature;
229
230    use super::*;
231
232    #[test]
233    fn graduated_feature_flag_gets_inserted() {
234        let mut project_config = ProjectConfig::default();
235        assert!(!project_config.features.has(Feature::UserReportV2Ingest));
236        project_config.sanitize();
237        assert!(project_config.features.has(Feature::UserReportV2Ingest));
238    }
239}