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#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(default, rename_all = "camelCase")]
22pub struct ProjectConfig {
23 pub allowed_domains: Vec<String>,
25 pub trusted_relays: Vec<PublicKey>,
27 #[serde(skip_serializing_if = "TrustedRelayConfig::is_empty")]
29 pub trusted_relay_settings: TrustedRelayConfig,
30 pub pii_config: Option<PiiConfig>,
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub grouping_config: Option<Value>,
35 #[serde(skip_serializing_if = "ProjectFiltersConfig::is_empty")]
37 pub filter_settings: ProjectFiltersConfig,
38 #[serde(skip_serializing_if = "DataScrubbingConfig::is_disabled")]
40 pub datascrubbing_settings: DataScrubbingConfig,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub event_retention: Option<u16>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub downsampled_event_retention: Option<u16>,
47 #[serde(default, skip_serializing_if = "RetentionsConfig::is_empty")]
49 pub retentions: RetentionsConfig,
50 #[serde(default, skip_serializing_if = "TrimmingConfigs::is_empty")]
52 pub trimming: TrimmingConfigs,
53 #[serde(skip_serializing_if = "Vec::is_empty")]
55 pub quotas: Vec<Quota>,
56 #[serde(alias = "dynamicSampling", skip_serializing_if = "Option::is_none")]
58 pub sampling: Option<ErrorBoundary<SamplingConfig>>,
59 #[serde(skip_serializing_if = "Option::is_none")]
62 pub measurements: Option<MeasurementsConfig>,
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub breakdowns_v2: Option<BreakdownsConfig>,
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub performance_score: Option<PerformanceScoreConfig>,
69 #[serde(skip_serializing_if = "SessionMetricsConfig::is_disabled")]
71 pub session_metrics: SessionMetricsConfig,
72 #[serde(default, skip_serializing_if = "skip_metrics_extraction")]
74 pub metric_extraction: ErrorBoundary<MetricExtractionConfig>,
75 #[serde(skip_serializing_if = "Vec::is_empty")]
77 pub metric_conditional_tagging: Vec<TaggingRule>,
78 #[serde(skip_serializing_if = "FeatureSet::is_empty")]
80 pub features: FeatureSet,
81 #[serde(skip_serializing_if = "Vec::is_empty")]
83 pub tx_name_rules: Vec<TransactionNameRule>,
84 #[serde(skip_serializing_if = "is_false")]
86 pub tx_name_ready: bool,
87 #[serde(skip_serializing_if = "Option::is_none")]
92 pub span_description_rules: Option<Vec<SpanDescriptionRule>>,
93}
94
95impl ProjectConfig {
96 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 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#[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 #[serde(skip_serializing_if = "is_false")]
216 pub tx_name_ready: bool,
217 #[serde(skip_serializing_if = "Option::is_none")]
222 pub span_description_rules: Option<Vec<SpanDescriptionRule>>,
223}
224
225#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
227pub struct RetentionConfig {
228 pub standard: u16,
230 #[serde(skip_serializing_if = "Option::is_none")]
232 pub downsampled: Option<u16>,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize, Default)]
237#[serde(rename_all = "camelCase")]
238pub struct RetentionsConfig {
239 #[serde(skip_serializing_if = "Option::is_none")]
241 pub log: Option<RetentionConfig>,
242 #[serde(skip_serializing_if = "Option::is_none")]
244 pub span: Option<RetentionConfig>,
245 #[serde(skip_serializing_if = "Option::is_none")]
247 pub trace_metric: Option<RetentionConfig>,
248 #[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#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
268#[serde(rename_all = "camelCase")]
269pub struct TrimmingConfig {
270 pub max_size: u32,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize, Default)]
276#[serde(rename_all = "camelCase")]
277pub struct TrimmingConfigs {
278 #[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}