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    /// Retention settings for different products.
51    #[serde(default, skip_serializing_if = "RetentionsConfig::is_empty")]
52    pub retentions: RetentionsConfig,
53    /// Usage quotas for this project.
54    #[serde(skip_serializing_if = "Vec::is_empty")]
55    pub quotas: Vec<Quota>,
56    /// Configuration for sampling traces, if not present there will be no sampling.
57    #[serde(alias = "dynamicSampling", skip_serializing_if = "Option::is_none")]
58    pub sampling: Option<ErrorBoundary<SamplingConfig>>,
59    /// Configuration for measurements.
60    /// NOTE: do not access directly, use [`relay_event_normalization::CombinedMeasurementsConfig`].
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub measurements: Option<MeasurementsConfig>,
63    /// Configuration for operation breakdown. Will be emitted only if present.
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub breakdowns_v2: Option<BreakdownsConfig>,
66    /// Configuration for performance score calculations. Will be emitted only if present.
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub performance_score: Option<PerformanceScoreConfig>,
69    /// Configuration for extracting metrics from sessions.
70    #[serde(skip_serializing_if = "SessionMetricsConfig::is_disabled")]
71    pub session_metrics: SessionMetricsConfig,
72    /// Configuration for extracting metrics from transaction events.
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub transaction_metrics: Option<ErrorBoundary<TransactionMetricsConfig>>,
75    /// Configuration for generic metrics extraction from all data categories.
76    #[serde(default, skip_serializing_if = "skip_metrics_extraction")]
77    pub metric_extraction: ErrorBoundary<MetricExtractionConfig>,
78    /// Rules for applying metrics tags depending on the event's content.
79    #[serde(skip_serializing_if = "Vec::is_empty")]
80    pub metric_conditional_tagging: Vec<TaggingRule>,
81    /// Exposable features enabled for this project.
82    #[serde(skip_serializing_if = "FeatureSet::is_empty")]
83    pub features: FeatureSet,
84    /// Transaction renaming rules.
85    #[serde(skip_serializing_if = "Vec::is_empty")]
86    pub tx_name_rules: Vec<TransactionNameRule>,
87    /// Whether or not a project is ready to mark all URL transactions as "sanitized".
88    #[serde(skip_serializing_if = "is_false")]
89    pub tx_name_ready: bool,
90    /// Span description renaming rules.
91    ///
92    /// These are currently not used by Relay, and only here to be forwarded to old
93    /// relays that might still need them.
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub span_description_rules: Option<Vec<SpanDescriptionRule>>,
96    /// Configuration for metrics.
97    #[serde(default, skip_serializing_if = "skip_metrics")]
98    pub metrics: ErrorBoundary<Metrics>,
99}
100
101impl ProjectConfig {
102    /// Validates fields in this project config and removes values that are partially invalid.
103    pub fn sanitize(&mut self) {
104        self.quotas.retain(Quota::is_valid);
105
106        metrics::convert_conditional_tagging(self);
107        defaults::add_span_metrics(self);
108
109        if let Some(ErrorBoundary::Ok(ref mut sampling_config)) = self.sampling {
110            sampling_config.normalize();
111        }
112
113        for flag in GRADUATED_FEATURE_FLAGS {
114            self.features.0.insert(*flag);
115        }
116
117        // Check if indexed and non-indexed are double-counting towards the same ID.
118        // This is probably not intended behavior.
119        for quota in &self.quotas {
120            if let Some(id) = &quota.id {
121                for category in &quota.categories {
122                    if let Some(indexed) = category.index_category()
123                        && quota.categories.contains(&indexed)
124                    {
125                        relay_log::error!(
126                            tags.id = id,
127                            "Categories {category} and {indexed} share the same quota ID. This will double-count items.",
128                        );
129                    }
130                }
131            }
132        }
133    }
134}
135
136impl Default for ProjectConfig {
137    fn default() -> Self {
138        ProjectConfig {
139            allowed_domains: vec!["*".to_owned()],
140            trusted_relays: vec![],
141            trusted_relay_settings: TrustedRelayConfig::default(),
142            pii_config: None,
143            grouping_config: None,
144            filter_settings: ProjectFiltersConfig::default(),
145            datascrubbing_settings: DataScrubbingConfig::default(),
146            event_retention: None,
147            downsampled_event_retention: None,
148            retentions: Default::default(),
149            quotas: Vec::new(),
150            sampling: None,
151            measurements: None,
152            breakdowns_v2: None,
153            performance_score: Default::default(),
154            session_metrics: SessionMetricsConfig::default(),
155            transaction_metrics: None,
156            metric_extraction: Default::default(),
157            metric_conditional_tagging: Vec::new(),
158            features: Default::default(),
159            tx_name_rules: Vec::new(),
160            tx_name_ready: false,
161            span_description_rules: None,
162            metrics: Default::default(),
163        }
164    }
165}
166
167fn skip_metrics_extraction(boundary: &ErrorBoundary<MetricExtractionConfig>) -> bool {
168    match boundary {
169        ErrorBoundary::Err(_) => true,
170        ErrorBoundary::Ok(config) => !config.is_enabled(),
171    }
172}
173
174fn skip_metrics(boundary: &ErrorBoundary<Metrics>) -> bool {
175    match boundary {
176        ErrorBoundary::Err(_) => true,
177        ErrorBoundary::Ok(metrics) => metrics.is_empty(),
178    }
179}
180
181/// Subset of [`ProjectConfig`] that is passed to external Relays.
182///
183/// For documentation of the fields, see [`ProjectConfig`].
184#[allow(missing_docs)]
185#[derive(Debug, Serialize)]
186#[serde(rename_all = "camelCase", remote = "ProjectConfig")]
187pub struct LimitedProjectConfig {
188    pub allowed_domains: Vec<String>,
189    pub trusted_relays: Vec<PublicKey>,
190    pub pii_config: Option<PiiConfig>,
191    #[serde(skip_serializing_if = "ProjectFiltersConfig::is_empty")]
192    pub filter_settings: ProjectFiltersConfig,
193    #[serde(skip_serializing_if = "DataScrubbingConfig::is_disabled")]
194    pub datascrubbing_settings: DataScrubbingConfig,
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub sampling: Option<ErrorBoundary<SamplingConfig>>,
197    #[serde(skip_serializing_if = "SessionMetricsConfig::is_disabled")]
198    pub session_metrics: SessionMetricsConfig,
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub transaction_metrics: Option<ErrorBoundary<TransactionMetricsConfig>>,
201    #[serde(default, skip_serializing_if = "skip_metrics_extraction")]
202    pub metric_extraction: ErrorBoundary<MetricExtractionConfig>,
203    #[serde(skip_serializing_if = "Vec::is_empty")]
204    pub metric_conditional_tagging: Vec<TaggingRule>,
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub measurements: Option<MeasurementsConfig>,
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub breakdowns_v2: Option<BreakdownsConfig>,
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub performance_score: Option<PerformanceScoreConfig>,
211    #[serde(skip_serializing_if = "FeatureSet::is_empty")]
212    pub features: FeatureSet,
213    #[serde(skip_serializing_if = "Vec::is_empty")]
214    pub tx_name_rules: Vec<TransactionNameRule>,
215    /// Whether or not a project is ready to mark all URL transactions as "sanitized".
216    #[serde(skip_serializing_if = "is_false")]
217    pub tx_name_ready: bool,
218    /// Span description renaming rules.
219    ///
220    /// These are currently not used by Relay, and only here to be forwarded to old
221    /// relays that might still need them.
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub span_description_rules: Option<Vec<SpanDescriptionRule>>,
224}
225
226/// Per-Category settings for retention policy.
227#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
228pub struct RetentionConfig {
229    /// Standard / full fidelity retention policy in days.
230    pub standard: u16,
231    /// Downsampled retention policy in days.
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub downsampled: Option<u16>,
234}
235
236/// Settings for retention policy.
237#[derive(Debug, Clone, Serialize, Deserialize, Default)]
238pub struct RetentionsConfig {
239    /// Retention settings for logs.
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub log: Option<RetentionConfig>,
242    /// Retention settings for spans.
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub span: Option<RetentionConfig>,
245    /// Retention settings for metrics.
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub trace_metric: Option<RetentionConfig>,
248}
249
250impl RetentionsConfig {
251    fn is_empty(&self) -> bool {
252        let Self {
253            log,
254            span,
255            trace_metric,
256        } = self;
257
258        log.is_none() && span.is_none() && trace_metric.is_none()
259    }
260}
261
262fn is_false(value: &bool) -> bool {
263    !*value
264}
265
266#[cfg(test)]
267mod tests {
268    use crate::Feature;
269
270    use super::*;
271
272    #[test]
273    fn graduated_feature_flag_gets_inserted() {
274        let mut project_config = ProjectConfig::default();
275        assert!(!project_config.features.has(Feature::UserReportV2Ingest));
276        project_config.sanitize();
277        assert!(project_config.features.has(Feature::UserReportV2Ingest));
278    }
279}