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