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#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(default, rename_all = "camelCase")]
25pub struct ProjectConfig {
26 pub allowed_domains: Vec<String>,
28 pub trusted_relays: Vec<PublicKey>,
30 #[serde(skip_serializing_if = "TrustedRelayConfig::is_empty")]
32 pub trusted_relay_settings: TrustedRelayConfig,
33 pub pii_config: Option<PiiConfig>,
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub grouping_config: Option<Value>,
38 #[serde(skip_serializing_if = "ProjectFiltersConfig::is_empty")]
40 pub filter_settings: ProjectFiltersConfig,
41 #[serde(skip_serializing_if = "DataScrubbingConfig::is_disabled")]
43 pub datascrubbing_settings: DataScrubbingConfig,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub event_retention: Option<u16>,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub downsampled_event_retention: Option<u16>,
50 #[serde(default, skip_serializing_if = "RetentionsConfig::is_empty")]
52 pub retentions: RetentionsConfig,
53 #[serde(default, skip_serializing_if = "TrimmingConfigs::is_empty")]
55 pub trimming: TrimmingConfigs,
56 #[serde(skip_serializing_if = "Vec::is_empty")]
58 pub quotas: Vec<Quota>,
59 #[serde(alias = "dynamicSampling", skip_serializing_if = "Option::is_none")]
61 pub sampling: Option<ErrorBoundary<SamplingConfig>>,
62 #[serde(skip_serializing_if = "Option::is_none")]
65 pub measurements: Option<MeasurementsConfig>,
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub breakdowns_v2: Option<BreakdownsConfig>,
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub performance_score: Option<PerformanceScoreConfig>,
72 #[serde(skip_serializing_if = "SessionMetricsConfig::is_disabled")]
74 pub session_metrics: SessionMetricsConfig,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub transaction_metrics: Option<ErrorBoundary<TransactionMetricsConfig>>,
78 #[serde(default, skip_serializing_if = "skip_metrics_extraction")]
80 pub metric_extraction: ErrorBoundary<MetricExtractionConfig>,
81 #[serde(skip_serializing_if = "Vec::is_empty")]
83 pub metric_conditional_tagging: Vec<TaggingRule>,
84 #[serde(skip_serializing_if = "FeatureSet::is_empty")]
86 pub features: FeatureSet,
87 #[serde(skip_serializing_if = "Vec::is_empty")]
89 pub tx_name_rules: Vec<TransactionNameRule>,
90 #[serde(skip_serializing_if = "is_false")]
92 pub tx_name_ready: bool,
93 #[serde(skip_serializing_if = "Option::is_none")]
98 pub span_description_rules: Option<Vec<SpanDescriptionRule>>,
99 #[serde(default, skip_serializing_if = "skip_metrics")]
101 pub metrics: ErrorBoundary<Metrics>,
102}
103
104impl ProjectConfig {
105 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 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#[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 #[serde(skip_serializing_if = "is_false")]
236 pub tx_name_ready: bool,
237 #[serde(skip_serializing_if = "Option::is_none")]
242 pub span_description_rules: Option<Vec<SpanDescriptionRule>>,
243}
244
245#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
247pub struct RetentionConfig {
248 pub standard: u16,
250 #[serde(skip_serializing_if = "Option::is_none")]
252 pub downsampled: Option<u16>,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize, Default)]
257#[serde(rename_all = "camelCase")]
258pub struct RetentionsConfig {
259 #[serde(skip_serializing_if = "Option::is_none")]
261 pub log: Option<RetentionConfig>,
262 #[serde(skip_serializing_if = "Option::is_none")]
264 pub span: Option<RetentionConfig>,
265 #[serde(skip_serializing_if = "Option::is_none")]
267 pub trace_metric: Option<RetentionConfig>,
268 #[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#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase")]
289pub struct TrimmingConfig {
290 pub max_size: u32,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize, Default)]
296#[serde(rename_all = "camelCase")]
297pub struct TrimmingConfigs {
298 #[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}