Skip to main content

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::GRADUATED_FEATURE_FLAGS;
14use crate::error_boundary::ErrorBoundary;
15use crate::feature::FeatureSet;
16use crate::metrics::SessionMetricsConfig;
17use crate::trusted_relay::TrustedRelayConfig;
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    /// Exposable features enabled for this project.
73    #[serde(skip_serializing_if = "FeatureSet::is_empty")]
74    pub features: FeatureSet,
75    /// Transaction renaming rules.
76    #[serde(skip_serializing_if = "Vec::is_empty")]
77    pub tx_name_rules: Vec<TransactionNameRule>,
78    /// Whether or not a project is ready to mark all URL transactions as "sanitized".
79    #[serde(skip_serializing_if = "is_false")]
80    pub tx_name_ready: bool,
81    /// Span description renaming rules.
82    ///
83    /// These are currently not used by Relay, and only here to be forwarded to old
84    /// relays that might still need them.
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub span_description_rules: Option<Vec<SpanDescriptionRule>>,
87}
88
89impl ProjectConfig {
90    /// Validates fields in this project config and removes values that are partially invalid.
91    pub fn sanitize(&mut self, report_errors: bool) {
92        self.remove_invalid_quotas(report_errors);
93
94        if let Some(ErrorBoundary::Ok(ref mut sampling_config)) = self.sampling {
95            sampling_config.normalize();
96        }
97
98        for flag in GRADUATED_FEATURE_FLAGS {
99            self.features.0.insert(*flag);
100        }
101    }
102
103    fn remove_invalid_quotas(&mut self, report_errors: bool) {
104        let invalid_quotas: Vec<_> = self.quotas.extract_if(.., |q| !q.is_valid()).collect();
105        if report_errors {
106            if !invalid_quotas.is_empty() {
107                {
108                    relay_log::warn!(
109                        invalid_quotas = ?invalid_quotas,
110                        "Found an invalid quota definition",
111                    );
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.as_deref() {
118                    for category in quota.categories.iter() {
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}
133
134impl Default for ProjectConfig {
135    fn default() -> Self {
136        ProjectConfig {
137            allowed_domains: vec!["*".to_owned()],
138            trusted_relays: vec![],
139            trusted_relay_settings: TrustedRelayConfig::default(),
140            pii_config: None,
141            grouping_config: None,
142            filter_settings: ProjectFiltersConfig::default(),
143            datascrubbing_settings: DataScrubbingConfig::default(),
144            event_retention: None,
145            downsampled_event_retention: None,
146            retentions: Default::default(),
147            trimming: Default::default(),
148            quotas: Vec::new(),
149            sampling: None,
150            measurements: None,
151            breakdowns_v2: None,
152            performance_score: Default::default(),
153            session_metrics: SessionMetricsConfig::default(),
154            features: Default::default(),
155            tx_name_rules: Vec::new(),
156            tx_name_ready: false,
157            span_description_rules: None,
158        }
159    }
160}
161
162/// Subset of [`ProjectConfig`] that is passed to external Relays.
163///
164/// For documentation of the fields, see [`ProjectConfig`].
165#[allow(missing_docs)]
166#[derive(Debug, Serialize)]
167#[serde(rename_all = "camelCase", remote = "ProjectConfig")]
168pub struct LimitedProjectConfig {
169    pub allowed_domains: Vec<String>,
170    pub trusted_relays: Vec<PublicKey>,
171    pub pii_config: Option<PiiConfig>,
172    #[serde(skip_serializing_if = "ProjectFiltersConfig::is_empty")]
173    pub filter_settings: ProjectFiltersConfig,
174    #[serde(skip_serializing_if = "DataScrubbingConfig::is_disabled")]
175    pub datascrubbing_settings: DataScrubbingConfig,
176    #[serde(skip_serializing_if = "TrimmingConfigs::is_empty")]
177    pub trimming: TrimmingConfigs,
178    #[serde(skip_serializing_if = "SessionMetricsConfig::is_disabled")]
179    pub session_metrics: SessionMetricsConfig,
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub measurements: Option<MeasurementsConfig>,
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub breakdowns_v2: Option<BreakdownsConfig>,
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub performance_score: Option<PerformanceScoreConfig>,
186    #[serde(skip_serializing_if = "FeatureSet::is_empty")]
187    pub features: FeatureSet,
188    #[serde(skip_serializing_if = "Vec::is_empty")]
189    pub tx_name_rules: Vec<TransactionNameRule>,
190    /// Whether or not a project is ready to mark all URL transactions as "sanitized".
191    #[serde(skip_serializing_if = "is_false")]
192    pub tx_name_ready: bool,
193    /// Span description renaming rules.
194    ///
195    /// These are currently not used by Relay, and only here to be forwarded to old
196    /// relays that might still need them.
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub span_description_rules: Option<Vec<SpanDescriptionRule>>,
199}
200
201/// Per-Category settings for retention policy.
202#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
203pub struct RetentionConfig {
204    /// Standard / full fidelity retention policy in days.
205    pub standard: u16,
206    /// Downsampled retention policy in days.
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub downsampled: Option<u16>,
209}
210
211/// Settings for retention policy.
212#[derive(Debug, Clone, Serialize, Deserialize, Default)]
213#[serde(rename_all = "camelCase")]
214pub struct RetentionsConfig {
215    /// Retention settings for logs.
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub log: Option<RetentionConfig>,
218    /// Retention settings for spans.
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub span: Option<RetentionConfig>,
221    /// Retention settings for metrics.
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub trace_metric: Option<RetentionConfig>,
224    /// Retention settings for attachments.
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub trace_attachment: Option<RetentionConfig>,
227}
228
229impl RetentionsConfig {
230    fn is_empty(&self) -> bool {
231        let Self {
232            log,
233            span,
234            trace_metric,
235            trace_attachment,
236        } = self;
237
238        log.is_none() && span.is_none() && trace_metric.is_none() && trace_attachment.is_none()
239    }
240}
241
242/// Per-category settings for item trimming.
243#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
244#[serde(rename_all = "camelCase")]
245pub struct TrimmingConfig {
246    /// The maximum size in bytes above which an item should be trimmed.
247    pub max_size: u32,
248}
249
250/// Settings for item trimming.
251#[derive(Debug, Clone, Serialize, Deserialize, Default)]
252#[serde(rename_all = "camelCase")]
253pub struct TrimmingConfigs {
254    /// Trimming settings for spans.
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub span: Option<TrimmingConfig>,
257}
258
259impl TrimmingConfigs {
260    fn is_empty(&self) -> bool {
261        let Self { span } = self;
262        span.is_none()
263    }
264}
265
266fn is_false(value: &bool) -> bool {
267    !*value
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    #[test]
275    fn graduated_feature_flag_gets_inserted() {
276        let mut project_config = ProjectConfig::default();
277        for feature in GRADUATED_FEATURE_FLAGS {
278            assert!(!project_config.features.has(*feature));
279        }
280
281        project_config.sanitize(false);
282
283        for feature in GRADUATED_FEATURE_FLAGS {
284            assert!(project_config.features.has(*feature));
285        }
286    }
287}