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#[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(skip_serializing_if = "FeatureSet::is_empty")]
74 pub features: FeatureSet,
75 #[serde(skip_serializing_if = "Vec::is_empty")]
77 pub tx_name_rules: Vec<TransactionNameRule>,
78 #[serde(skip_serializing_if = "is_false")]
80 pub tx_name_ready: bool,
81 #[serde(skip_serializing_if = "Option::is_none")]
86 pub span_description_rules: Option<Vec<SpanDescriptionRule>>,
87}
88
89impl ProjectConfig {
90 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 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#[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 #[serde(skip_serializing_if = "is_false")]
192 pub tx_name_ready: bool,
193 #[serde(skip_serializing_if = "Option::is_none")]
198 pub span_description_rules: Option<Vec<SpanDescriptionRule>>,
199}
200
201#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
203pub struct RetentionConfig {
204 pub standard: u16,
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub downsampled: Option<u16>,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize, Default)]
213#[serde(rename_all = "camelCase")]
214pub struct RetentionsConfig {
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub log: Option<RetentionConfig>,
218 #[serde(skip_serializing_if = "Option::is_none")]
220 pub span: Option<RetentionConfig>,
221 #[serde(skip_serializing_if = "Option::is_none")]
223 pub trace_metric: Option<RetentionConfig>,
224 #[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#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
244#[serde(rename_all = "camelCase")]
245pub struct TrimmingConfig {
246 pub max_size: u32,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize, Default)]
252#[serde(rename_all = "camelCase")]
253pub struct TrimmingConfigs {
254 #[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}