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(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(skip_serializing_if = "Option::is_none")]
74 pub transaction_metrics: Option<ErrorBoundary<TransactionMetricsConfig>>,
75 #[serde(default, skip_serializing_if = "skip_metrics_extraction")]
77 pub metric_extraction: ErrorBoundary<MetricExtractionConfig>,
78 #[serde(skip_serializing_if = "Vec::is_empty")]
80 pub metric_conditional_tagging: Vec<TaggingRule>,
81 #[serde(skip_serializing_if = "FeatureSet::is_empty")]
83 pub features: FeatureSet,
84 #[serde(skip_serializing_if = "Vec::is_empty")]
86 pub tx_name_rules: Vec<TransactionNameRule>,
87 #[serde(skip_serializing_if = "is_false")]
89 pub tx_name_ready: bool,
90 #[serde(skip_serializing_if = "Option::is_none")]
95 pub span_description_rules: Option<Vec<SpanDescriptionRule>>,
96 #[serde(default, skip_serializing_if = "skip_metrics")]
98 pub metrics: ErrorBoundary<Metrics>,
99}
100
101impl ProjectConfig {
102 pub fn sanitize(&mut self, report_errors: bool) {
104 self.remove_invalid_quotas(report_errors);
105
106 metrics::convert_conditional_tagging(self);
107 defaults::add_span_metrics(self);
108
109 if let Some(ErrorBoundary::Ok(ref mut sampling_config)) = self.sampling {
110 sampling_config.normalize();
111 }
112
113 for flag in GRADUATED_FEATURE_FLAGS {
114 self.features.0.insert(*flag);
115 }
116 }
117
118 fn remove_invalid_quotas(&mut self, report_errors: bool) {
119 let invalid_quotas: Vec<_> = self.quotas.extract_if(.., |q| !q.is_valid()).collect();
120 if report_errors {
121 if !invalid_quotas.is_empty() {
122 {
123 relay_log::warn!(
124 invalid_quotas = ?invalid_quotas,
125 "Found an invalid quota definition",
126 );
127 }
128 }
129 for quota in &self.quotas {
132 if let Some(id) = quota.id.as_deref() {
133 for category in &*quota.categories {
134 if let Some(indexed) = category.index_category()
135 && quota.categories.contains(&indexed)
136 {
137 relay_log::error!(
138 tags.id = id,
139 "Categories {category} and {indexed} share the same quota ID. This will double-count items.",
140 );
141 }
142 }
143 }
144 }
145 }
146 }
147}
148
149impl Default for ProjectConfig {
150 fn default() -> Self {
151 ProjectConfig {
152 allowed_domains: vec!["*".to_owned()],
153 trusted_relays: vec![],
154 trusted_relay_settings: TrustedRelayConfig::default(),
155 pii_config: None,
156 grouping_config: None,
157 filter_settings: ProjectFiltersConfig::default(),
158 datascrubbing_settings: DataScrubbingConfig::default(),
159 event_retention: None,
160 downsampled_event_retention: None,
161 retentions: Default::default(),
162 quotas: Vec::new(),
163 sampling: None,
164 measurements: None,
165 breakdowns_v2: None,
166 performance_score: Default::default(),
167 session_metrics: SessionMetricsConfig::default(),
168 transaction_metrics: None,
169 metric_extraction: Default::default(),
170 metric_conditional_tagging: Vec::new(),
171 features: Default::default(),
172 tx_name_rules: Vec::new(),
173 tx_name_ready: false,
174 span_description_rules: None,
175 metrics: Default::default(),
176 }
177 }
178}
179
180fn skip_metrics_extraction(boundary: &ErrorBoundary<MetricExtractionConfig>) -> bool {
181 match boundary {
182 ErrorBoundary::Err(_) => true,
183 ErrorBoundary::Ok(config) => !config.is_enabled(),
184 }
185}
186
187fn skip_metrics(boundary: &ErrorBoundary<Metrics>) -> bool {
188 match boundary {
189 ErrorBoundary::Err(_) => true,
190 ErrorBoundary::Ok(metrics) => metrics.is_empty(),
191 }
192}
193
194#[allow(missing_docs)]
198#[derive(Debug, Serialize)]
199#[serde(rename_all = "camelCase", remote = "ProjectConfig")]
200pub struct LimitedProjectConfig {
201 pub allowed_domains: Vec<String>,
202 pub trusted_relays: Vec<PublicKey>,
203 pub pii_config: Option<PiiConfig>,
204 #[serde(skip_serializing_if = "ProjectFiltersConfig::is_empty")]
205 pub filter_settings: ProjectFiltersConfig,
206 #[serde(skip_serializing_if = "DataScrubbingConfig::is_disabled")]
207 pub datascrubbing_settings: DataScrubbingConfig,
208 #[serde(skip_serializing_if = "Option::is_none")]
209 pub sampling: Option<ErrorBoundary<SamplingConfig>>,
210 #[serde(skip_serializing_if = "SessionMetricsConfig::is_disabled")]
211 pub session_metrics: SessionMetricsConfig,
212 #[serde(skip_serializing_if = "Option::is_none")]
213 pub transaction_metrics: Option<ErrorBoundary<TransactionMetricsConfig>>,
214 #[serde(default, skip_serializing_if = "skip_metrics_extraction")]
215 pub metric_extraction: ErrorBoundary<MetricExtractionConfig>,
216 #[serde(skip_serializing_if = "Vec::is_empty")]
217 pub metric_conditional_tagging: Vec<TaggingRule>,
218 #[serde(skip_serializing_if = "Option::is_none")]
219 pub measurements: Option<MeasurementsConfig>,
220 #[serde(skip_serializing_if = "Option::is_none")]
221 pub breakdowns_v2: Option<BreakdownsConfig>,
222 #[serde(skip_serializing_if = "Option::is_none")]
223 pub performance_score: Option<PerformanceScoreConfig>,
224 #[serde(skip_serializing_if = "FeatureSet::is_empty")]
225 pub features: FeatureSet,
226 #[serde(skip_serializing_if = "Vec::is_empty")]
227 pub tx_name_rules: Vec<TransactionNameRule>,
228 #[serde(skip_serializing_if = "is_false")]
230 pub tx_name_ready: bool,
231 #[serde(skip_serializing_if = "Option::is_none")]
236 pub span_description_rules: Option<Vec<SpanDescriptionRule>>,
237}
238
239#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
241pub struct RetentionConfig {
242 pub standard: u16,
244 #[serde(skip_serializing_if = "Option::is_none")]
246 pub downsampled: Option<u16>,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize, Default)]
251#[serde(rename_all = "camelCase")]
252pub struct RetentionsConfig {
253 #[serde(skip_serializing_if = "Option::is_none")]
255 pub log: Option<RetentionConfig>,
256 #[serde(skip_serializing_if = "Option::is_none")]
258 pub span: Option<RetentionConfig>,
259 #[serde(skip_serializing_if = "Option::is_none")]
261 pub trace_metric: Option<RetentionConfig>,
262 #[serde(skip_serializing_if = "Option::is_none")]
264 pub trace_attachment: Option<RetentionConfig>,
265}
266
267impl RetentionsConfig {
268 fn is_empty(&self) -> bool {
269 let Self {
270 log,
271 span,
272 trace_metric,
273 trace_attachment,
274 } = self;
275
276 log.is_none() && span.is_none() && trace_metric.is_none() && trace_attachment.is_none()
277 }
278}
279
280fn is_false(value: &bool) -> bool {
281 !*value
282}
283
284#[cfg(test)]
285mod tests {
286 use crate::Feature;
287
288 use super::*;
289
290 #[test]
291 fn graduated_feature_flag_gets_inserted() {
292 let mut project_config = ProjectConfig::default();
293 assert!(!project_config.features.has(Feature::UserReportV2Ingest));
294 project_config.sanitize(false);
295 assert!(project_config.features.has(Feature::UserReportV2Ingest));
296 }
297}