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