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) {
104 self.quotas.retain(Quota::is_valid);
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 for quota in &self.quotas {
120 if let Some(id) = "a.id {
121 for category in "a.categories {
122 if let Some(indexed) = category.index_category()
123 && quota.categories.contains(&indexed)
124 {
125 relay_log::error!(
126 tags.id = id,
127 "Categories {category} and {indexed} share the same quota ID. This will double-count items.",
128 );
129 }
130 }
131 }
132 }
133 }
134}
135
136impl Default for ProjectConfig {
137 fn default() -> Self {
138 ProjectConfig {
139 allowed_domains: vec!["*".to_owned()],
140 trusted_relays: vec![],
141 trusted_relay_settings: TrustedRelayConfig::default(),
142 pii_config: None,
143 grouping_config: None,
144 filter_settings: ProjectFiltersConfig::default(),
145 datascrubbing_settings: DataScrubbingConfig::default(),
146 event_retention: None,
147 downsampled_event_retention: None,
148 retentions: Default::default(),
149 quotas: Vec::new(),
150 sampling: None,
151 measurements: None,
152 breakdowns_v2: None,
153 performance_score: Default::default(),
154 session_metrics: SessionMetricsConfig::default(),
155 transaction_metrics: None,
156 metric_extraction: Default::default(),
157 metric_conditional_tagging: Vec::new(),
158 features: Default::default(),
159 tx_name_rules: Vec::new(),
160 tx_name_ready: false,
161 span_description_rules: None,
162 metrics: Default::default(),
163 }
164 }
165}
166
167fn skip_metrics_extraction(boundary: &ErrorBoundary<MetricExtractionConfig>) -> bool {
168 match boundary {
169 ErrorBoundary::Err(_) => true,
170 ErrorBoundary::Ok(config) => !config.is_enabled(),
171 }
172}
173
174fn skip_metrics(boundary: &ErrorBoundary<Metrics>) -> bool {
175 match boundary {
176 ErrorBoundary::Err(_) => true,
177 ErrorBoundary::Ok(metrics) => metrics.is_empty(),
178 }
179}
180
181#[allow(missing_docs)]
185#[derive(Debug, Serialize)]
186#[serde(rename_all = "camelCase", remote = "ProjectConfig")]
187pub struct LimitedProjectConfig {
188 pub allowed_domains: Vec<String>,
189 pub trusted_relays: Vec<PublicKey>,
190 pub pii_config: Option<PiiConfig>,
191 #[serde(skip_serializing_if = "ProjectFiltersConfig::is_empty")]
192 pub filter_settings: ProjectFiltersConfig,
193 #[serde(skip_serializing_if = "DataScrubbingConfig::is_disabled")]
194 pub datascrubbing_settings: DataScrubbingConfig,
195 #[serde(skip_serializing_if = "Option::is_none")]
196 pub sampling: Option<ErrorBoundary<SamplingConfig>>,
197 #[serde(skip_serializing_if = "SessionMetricsConfig::is_disabled")]
198 pub session_metrics: SessionMetricsConfig,
199 #[serde(skip_serializing_if = "Option::is_none")]
200 pub transaction_metrics: Option<ErrorBoundary<TransactionMetricsConfig>>,
201 #[serde(default, skip_serializing_if = "skip_metrics_extraction")]
202 pub metric_extraction: ErrorBoundary<MetricExtractionConfig>,
203 #[serde(skip_serializing_if = "Vec::is_empty")]
204 pub metric_conditional_tagging: Vec<TaggingRule>,
205 #[serde(skip_serializing_if = "Option::is_none")]
206 pub measurements: Option<MeasurementsConfig>,
207 #[serde(skip_serializing_if = "Option::is_none")]
208 pub breakdowns_v2: Option<BreakdownsConfig>,
209 #[serde(skip_serializing_if = "Option::is_none")]
210 pub performance_score: Option<PerformanceScoreConfig>,
211 #[serde(skip_serializing_if = "FeatureSet::is_empty")]
212 pub features: FeatureSet,
213 #[serde(skip_serializing_if = "Vec::is_empty")]
214 pub tx_name_rules: Vec<TransactionNameRule>,
215 #[serde(skip_serializing_if = "is_false")]
217 pub tx_name_ready: bool,
218 #[serde(skip_serializing_if = "Option::is_none")]
223 pub span_description_rules: Option<Vec<SpanDescriptionRule>>,
224}
225
226#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
228pub struct RetentionConfig {
229 pub standard: u16,
231 #[serde(skip_serializing_if = "Option::is_none")]
233 pub downsampled: Option<u16>,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize, Default)]
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}
249
250impl RetentionsConfig {
251 fn is_empty(&self) -> bool {
252 let Self {
253 log,
254 span,
255 trace_metric,
256 } = self;
257
258 log.is_none() && span.is_none() && trace_metric.is_none()
259 }
260}
261
262fn is_false(value: &bool) -> bool {
263 !*value
264}
265
266#[cfg(test)]
267mod tests {
268 use crate::Feature;
269
270 use super::*;
271
272 #[test]
273 fn graduated_feature_flag_gets_inserted() {
274 let mut project_config = ProjectConfig::default();
275 assert!(!project_config.features.has(Feature::UserReportV2Ingest));
276 project_config.sanitize();
277 assert!(project_config.features.has(Feature::UserReportV2Ingest));
278 }
279}