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::{self, MetricExtractionConfig, SessionMetricsConfig, TaggingRule};
16use crate::trusted_relay::TrustedRelayConfig;
17use crate::{GRADUATED_FEATURE_FLAGS, defaults};
18
19/// Dynamic, per-DSN configuration passed down from Sentry.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(default, rename_all = "camelCase")]
22pub struct ProjectConfig {
23    /// URLs that are permitted for cross original JavaScript requests.
24    pub allowed_domains: Vec<String>,
25    /// List of relay public keys that are permitted to access this project.
26    pub trusted_relays: Vec<PublicKey>,
27    /// Configuration for trusted Relay behaviour.
28    #[serde(skip_serializing_if = "TrustedRelayConfig::is_empty")]
29    pub trusted_relay_settings: TrustedRelayConfig,
30    /// Configuration for PII stripping.
31    pub pii_config: Option<PiiConfig>,
32    /// The grouping configuration.
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub grouping_config: Option<Value>,
35    /// Configuration for filter rules.
36    #[serde(skip_serializing_if = "ProjectFiltersConfig::is_empty")]
37    pub filter_settings: ProjectFiltersConfig,
38    /// Configuration for data scrubbers.
39    #[serde(skip_serializing_if = "DataScrubbingConfig::is_disabled")]
40    pub datascrubbing_settings: DataScrubbingConfig,
41    /// Maximum event retention for the organization.
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub event_retention: Option<u16>,
44    /// Maximum sampled event retention for the organization.
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub downsampled_event_retention: Option<u16>,
47    /// Retention settings for different products.
48    #[serde(default, skip_serializing_if = "RetentionsConfig::is_empty")]
49    pub retentions: RetentionsConfig,
50    /// Trimming settings for different products.
51    #[serde(default, skip_serializing_if = "TrimmingConfigs::is_empty")]
52    pub trimming: TrimmingConfigs,
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 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}
94
95impl ProjectConfig {
96    /// Validates fields in this project config and removes values that are partially invalid.
97    pub fn sanitize(&mut self, report_errors: bool) {
98        self.remove_invalid_quotas(report_errors);
99
100        metrics::convert_conditional_tagging(self);
101        defaults::add_span_metrics(self);
102
103        if let Some(ErrorBoundary::Ok(ref mut sampling_config)) = self.sampling {
104            sampling_config.normalize();
105        }
106
107        for flag in GRADUATED_FEATURE_FLAGS {
108            self.features.0.insert(*flag);
109        }
110    }
111
112    fn remove_invalid_quotas(&mut self, report_errors: bool) {
113        let invalid_quotas: Vec<_> = self.quotas.extract_if(.., |q| !q.is_valid()).collect();
114        if report_errors {
115            if !invalid_quotas.is_empty() {
116                {
117                    relay_log::warn!(
118                        invalid_quotas = ?invalid_quotas,
119                        "Found an invalid quota definition",
120                    );
121                }
122            }
123            // Check if indexed and non-indexed are double-counting towards the same ID.
124            // This is probably not intended behavior.
125            for quota in &self.quotas {
126                if let Some(id) = quota.id.as_deref() {
127                    for category in &*quota.categories {
128                        if let Some(indexed) = category.index_category()
129                            && quota.categories.contains(&indexed)
130                        {
131                            relay_log::error!(
132                                tags.id = id,
133                                "Categories {category} and {indexed} share the same quota ID. This will double-count items.",
134                            );
135                        }
136                    }
137                }
138            }
139        }
140    }
141}
142
143impl Default for ProjectConfig {
144    fn default() -> Self {
145        ProjectConfig {
146            allowed_domains: vec!["*".to_owned()],
147            trusted_relays: vec![],
148            trusted_relay_settings: TrustedRelayConfig::default(),
149            pii_config: None,
150            grouping_config: None,
151            filter_settings: ProjectFiltersConfig::default(),
152            datascrubbing_settings: DataScrubbingConfig::default(),
153            event_retention: None,
154            downsampled_event_retention: None,
155            retentions: Default::default(),
156            trimming: Default::default(),
157            quotas: Vec::new(),
158            sampling: None,
159            measurements: None,
160            breakdowns_v2: None,
161            performance_score: Default::default(),
162            session_metrics: SessionMetricsConfig::default(),
163            metric_extraction: Default::default(),
164            metric_conditional_tagging: Vec::new(),
165            features: Default::default(),
166            tx_name_rules: Vec::new(),
167            tx_name_ready: false,
168            span_description_rules: None,
169        }
170    }
171}
172
173fn skip_metrics_extraction(boundary: &ErrorBoundary<MetricExtractionConfig>) -> bool {
174    match boundary {
175        ErrorBoundary::Err(_) => true,
176        ErrorBoundary::Ok(config) => !config.is_enabled(),
177    }
178}
179
180/// Subset of [`ProjectConfig`] that is passed to external Relays.
181///
182/// For documentation of the fields, see [`ProjectConfig`].
183#[allow(missing_docs)]
184#[derive(Debug, Serialize)]
185#[serde(rename_all = "camelCase", remote = "ProjectConfig")]
186pub struct LimitedProjectConfig {
187    pub allowed_domains: Vec<String>,
188    pub trusted_relays: Vec<PublicKey>,
189    pub pii_config: Option<PiiConfig>,
190    #[serde(skip_serializing_if = "ProjectFiltersConfig::is_empty")]
191    pub filter_settings: ProjectFiltersConfig,
192    #[serde(skip_serializing_if = "DataScrubbingConfig::is_disabled")]
193    pub datascrubbing_settings: DataScrubbingConfig,
194    #[serde(skip_serializing_if = "TrimmingConfigs::is_empty")]
195    pub trimming: TrimmingConfigs,
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub sampling: Option<ErrorBoundary<SamplingConfig>>,
198    #[serde(skip_serializing_if = "SessionMetricsConfig::is_disabled")]
199    pub session_metrics: SessionMetricsConfig,
200    #[serde(default, skip_serializing_if = "skip_metrics_extraction")]
201    pub metric_extraction: ErrorBoundary<MetricExtractionConfig>,
202    #[serde(skip_serializing_if = "Vec::is_empty")]
203    pub metric_conditional_tagging: Vec<TaggingRule>,
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub measurements: Option<MeasurementsConfig>,
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub breakdowns_v2: Option<BreakdownsConfig>,
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub performance_score: Option<PerformanceScoreConfig>,
210    #[serde(skip_serializing_if = "FeatureSet::is_empty")]
211    pub features: FeatureSet,
212    #[serde(skip_serializing_if = "Vec::is_empty")]
213    pub tx_name_rules: Vec<TransactionNameRule>,
214    /// Whether or not a project is ready to mark all URL transactions as "sanitized".
215    #[serde(skip_serializing_if = "is_false")]
216    pub tx_name_ready: bool,
217    /// Span description renaming rules.
218    ///
219    /// These are currently not used by Relay, and only here to be forwarded to old
220    /// relays that might still need them.
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub span_description_rules: Option<Vec<SpanDescriptionRule>>,
223}
224
225/// Per-Category settings for retention policy.
226#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
227pub struct RetentionConfig {
228    /// Standard / full fidelity retention policy in days.
229    pub standard: u16,
230    /// Downsampled retention policy in days.
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub downsampled: Option<u16>,
233}
234
235/// Settings for retention policy.
236#[derive(Debug, Clone, Serialize, Deserialize, Default)]
237#[serde(rename_all = "camelCase")]
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    /// Retention settings for attachments.
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub trace_attachment: Option<RetentionConfig>,
251}
252
253impl RetentionsConfig {
254    fn is_empty(&self) -> bool {
255        let Self {
256            log,
257            span,
258            trace_metric,
259            trace_attachment,
260        } = self;
261
262        log.is_none() && span.is_none() && trace_metric.is_none() && trace_attachment.is_none()
263    }
264}
265
266/// Per-category settings for item trimming.
267#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
268#[serde(rename_all = "camelCase")]
269pub struct TrimmingConfig {
270    /// The maximum size in bytes above which an item should be trimmed.
271    pub max_size: u32,
272}
273
274/// Settings for item trimming.
275#[derive(Debug, Clone, Serialize, Deserialize, Default)]
276#[serde(rename_all = "camelCase")]
277pub struct TrimmingConfigs {
278    /// Trimming settings for spans.
279    #[serde(skip_serializing_if = "Option::is_none")]
280    pub span: Option<TrimmingConfig>,
281}
282
283impl TrimmingConfigs {
284    fn is_empty(&self) -> bool {
285        let Self { span } = self;
286        span.is_none()
287    }
288}
289
290fn is_false(value: &bool) -> bool {
291    !*value
292}
293
294#[cfg(test)]
295mod tests {
296    use crate::Feature;
297
298    use super::*;
299
300    #[test]
301    fn graduated_feature_flag_gets_inserted() {
302        let mut project_config = ProjectConfig::default();
303        assert!(!project_config.features.has(Feature::UserReportV2Ingest));
304        project_config.sanitize(false);
305        assert!(project_config.features.has(Feature::UserReportV2Ingest));
306    }
307}