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::{defaults, GRADUATED_FEATURE_FLAGS};
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(default, rename_all = "camelCase")]
24pub struct ProjectConfig {
25 pub allowed_domains: Vec<String>,
27 pub trusted_relays: Vec<PublicKey>,
29 pub pii_config: Option<PiiConfig>,
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub grouping_config: Option<Value>,
34 #[serde(skip_serializing_if = "ProjectFiltersConfig::is_empty")]
36 pub filter_settings: ProjectFiltersConfig,
37 #[serde(skip_serializing_if = "DataScrubbingConfig::is_disabled")]
39 pub datascrubbing_settings: DataScrubbingConfig,
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub event_retention: Option<u16>,
43 #[serde(skip_serializing_if = "Vec::is_empty")]
45 pub quotas: Vec<Quota>,
46 #[serde(alias = "dynamicSampling", skip_serializing_if = "Option::is_none")]
48 pub sampling: Option<ErrorBoundary<SamplingConfig>>,
49 #[serde(skip_serializing_if = "Option::is_none")]
52 pub measurements: Option<MeasurementsConfig>,
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub breakdowns_v2: Option<BreakdownsConfig>,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub performance_score: Option<PerformanceScoreConfig>,
59 #[serde(skip_serializing_if = "SessionMetricsConfig::is_disabled")]
61 pub session_metrics: SessionMetricsConfig,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub transaction_metrics: Option<ErrorBoundary<TransactionMetricsConfig>>,
65 #[serde(default, skip_serializing_if = "skip_metrics_extraction")]
67 pub metric_extraction: ErrorBoundary<MetricExtractionConfig>,
68 #[serde(skip_serializing_if = "Vec::is_empty")]
70 pub metric_conditional_tagging: Vec<TaggingRule>,
71 #[serde(skip_serializing_if = "FeatureSet::is_empty")]
73 pub features: FeatureSet,
74 #[serde(skip_serializing_if = "Vec::is_empty")]
76 pub tx_name_rules: Vec<TransactionNameRule>,
77 #[serde(skip_serializing_if = "is_false")]
79 pub tx_name_ready: bool,
80 #[serde(skip_serializing_if = "Option::is_none")]
85 pub span_description_rules: Option<Vec<SpanDescriptionRule>>,
86 #[serde(default, skip_serializing_if = "skip_metrics")]
88 pub metrics: ErrorBoundary<Metrics>,
89}
90
91impl ProjectConfig {
92 pub fn sanitize(&mut self) {
94 self.quotas.retain(Quota::is_valid);
95
96 metrics::convert_conditional_tagging(self);
97 defaults::add_span_metrics(self);
98
99 if let Some(ErrorBoundary::Ok(ref mut sampling_config)) = self.sampling {
100 sampling_config.normalize();
101 }
102
103 for flag in GRADUATED_FEATURE_FLAGS {
104 self.features.0.insert(*flag);
105 }
106
107 for quota in &self.quotas {
110 if let Some(id) = "a.id {
111 for category in "a.categories {
112 if let Some(indexed) = category.index_category() {
113 if quota.categories.contains(&indexed) {
114 relay_log::error!(
115 tags.id = id,
116 "Categories {category} and {indexed} share the same quota ID. This will double-count items.",
117 );
118 }
119 }
120 }
121 }
122 }
123 }
124}
125
126impl Default for ProjectConfig {
127 fn default() -> Self {
128 ProjectConfig {
129 allowed_domains: vec!["*".to_string()],
130 trusted_relays: vec![],
131 pii_config: None,
132 grouping_config: None,
133 filter_settings: ProjectFiltersConfig::default(),
134 datascrubbing_settings: DataScrubbingConfig::default(),
135 event_retention: None,
136 quotas: Vec::new(),
137 sampling: None,
138 measurements: None,
139 breakdowns_v2: None,
140 performance_score: Default::default(),
141 session_metrics: SessionMetricsConfig::default(),
142 transaction_metrics: None,
143 metric_extraction: Default::default(),
144 metric_conditional_tagging: Vec::new(),
145 features: Default::default(),
146 tx_name_rules: Vec::new(),
147 tx_name_ready: false,
148 span_description_rules: None,
149 metrics: Default::default(),
150 }
151 }
152}
153
154fn skip_metrics_extraction(boundary: &ErrorBoundary<MetricExtractionConfig>) -> bool {
155 match boundary {
156 ErrorBoundary::Err(_) => true,
157 ErrorBoundary::Ok(config) => !config.is_enabled(),
158 }
159}
160
161fn skip_metrics(boundary: &ErrorBoundary<Metrics>) -> bool {
162 match boundary {
163 ErrorBoundary::Err(_) => true,
164 ErrorBoundary::Ok(metrics) => metrics.is_empty(),
165 }
166}
167
168#[allow(missing_docs)]
172#[derive(Debug, Serialize)]
173#[serde(rename_all = "camelCase", remote = "ProjectConfig")]
174pub struct LimitedProjectConfig {
175 pub allowed_domains: Vec<String>,
176 pub trusted_relays: Vec<PublicKey>,
177 pub pii_config: Option<PiiConfig>,
178 #[serde(skip_serializing_if = "ProjectFiltersConfig::is_empty")]
179 pub filter_settings: ProjectFiltersConfig,
180 #[serde(skip_serializing_if = "DataScrubbingConfig::is_disabled")]
181 pub datascrubbing_settings: DataScrubbingConfig,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub sampling: Option<ErrorBoundary<SamplingConfig>>,
184 #[serde(skip_serializing_if = "SessionMetricsConfig::is_disabled")]
185 pub session_metrics: SessionMetricsConfig,
186 #[serde(skip_serializing_if = "Option::is_none")]
187 pub transaction_metrics: Option<ErrorBoundary<TransactionMetricsConfig>>,
188 #[serde(default, skip_serializing_if = "skip_metrics_extraction")]
189 pub metric_extraction: ErrorBoundary<MetricExtractionConfig>,
190 #[serde(skip_serializing_if = "Vec::is_empty")]
191 pub metric_conditional_tagging: Vec<TaggingRule>,
192 #[serde(skip_serializing_if = "Option::is_none")]
193 pub measurements: Option<MeasurementsConfig>,
194 #[serde(skip_serializing_if = "Option::is_none")]
195 pub breakdowns_v2: Option<BreakdownsConfig>,
196 #[serde(skip_serializing_if = "Option::is_none")]
197 pub performance_score: Option<PerformanceScoreConfig>,
198 #[serde(skip_serializing_if = "FeatureSet::is_empty")]
199 pub features: FeatureSet,
200 #[serde(skip_serializing_if = "Vec::is_empty")]
201 pub tx_name_rules: Vec<TransactionNameRule>,
202 #[serde(skip_serializing_if = "is_false")]
204 pub tx_name_ready: bool,
205 #[serde(skip_serializing_if = "Option::is_none")]
210 pub span_description_rules: Option<Vec<SpanDescriptionRule>>,
211}
212
213fn is_false(value: &bool) -> bool {
214 !*value
215}
216
217#[cfg(test)]
218mod tests {
219 use crate::Feature;
220
221 use super::*;
222
223 #[test]
224 fn graduated_feature_flag_gets_inserted() {
225 let mut project_config = ProjectConfig::default();
226 assert!(!project_config.features.has(Feature::UserReportV2Ingest));
227 project_config.sanitize();
228 assert!(project_config.features.has(Feature::UserReportV2Ingest));
229 }
230}