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(skip_serializing_if = "Vec::is_empty")]
52 pub quotas: Vec<Quota>,
53 #[serde(alias = "dynamicSampling", skip_serializing_if = "Option::is_none")]
55 pub sampling: Option<ErrorBoundary<SamplingConfig>>,
56 #[serde(skip_serializing_if = "Option::is_none")]
59 pub measurements: Option<MeasurementsConfig>,
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub breakdowns_v2: Option<BreakdownsConfig>,
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub performance_score: Option<PerformanceScoreConfig>,
66 #[serde(skip_serializing_if = "SessionMetricsConfig::is_disabled")]
68 pub session_metrics: SessionMetricsConfig,
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub transaction_metrics: Option<ErrorBoundary<TransactionMetricsConfig>>,
72 #[serde(default, skip_serializing_if = "skip_metrics_extraction")]
74 pub metric_extraction: ErrorBoundary<MetricExtractionConfig>,
75 #[serde(skip_serializing_if = "Vec::is_empty")]
77 pub metric_conditional_tagging: Vec<TaggingRule>,
78 #[serde(skip_serializing_if = "FeatureSet::is_empty")]
80 pub features: FeatureSet,
81 #[serde(skip_serializing_if = "Vec::is_empty")]
83 pub tx_name_rules: Vec<TransactionNameRule>,
84 #[serde(skip_serializing_if = "is_false")]
86 pub tx_name_ready: bool,
87 #[serde(skip_serializing_if = "Option::is_none")]
92 pub span_description_rules: Option<Vec<SpanDescriptionRule>>,
93 #[serde(default, skip_serializing_if = "skip_metrics")]
95 pub metrics: ErrorBoundary<Metrics>,
96}
97
98impl ProjectConfig {
99 pub fn sanitize(&mut self) {
101 self.quotas.retain(Quota::is_valid);
102
103 metrics::convert_conditional_tagging(self);
104 defaults::add_span_metrics(self);
105
106 if let Some(ErrorBoundary::Ok(ref mut sampling_config)) = self.sampling {
107 sampling_config.normalize();
108 }
109
110 for flag in GRADUATED_FEATURE_FLAGS {
111 self.features.0.insert(*flag);
112 }
113
114 for quota in &self.quotas {
117 if let Some(id) = "a.id {
118 for category in "a.categories {
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
133impl Default for ProjectConfig {
134 fn default() -> Self {
135 ProjectConfig {
136 allowed_domains: vec!["*".to_owned()],
137 trusted_relays: vec![],
138 trusted_relay_settings: TrustedRelayConfig::default(),
139 pii_config: None,
140 grouping_config: None,
141 filter_settings: ProjectFiltersConfig::default(),
142 datascrubbing_settings: DataScrubbingConfig::default(),
143 event_retention: None,
144 downsampled_event_retention: None,
145 quotas: Vec::new(),
146 sampling: None,
147 measurements: None,
148 breakdowns_v2: None,
149 performance_score: Default::default(),
150 session_metrics: SessionMetricsConfig::default(),
151 transaction_metrics: None,
152 metric_extraction: Default::default(),
153 metric_conditional_tagging: Vec::new(),
154 features: Default::default(),
155 tx_name_rules: Vec::new(),
156 tx_name_ready: false,
157 span_description_rules: None,
158 metrics: Default::default(),
159 }
160 }
161}
162
163fn skip_metrics_extraction(boundary: &ErrorBoundary<MetricExtractionConfig>) -> bool {
164 match boundary {
165 ErrorBoundary::Err(_) => true,
166 ErrorBoundary::Ok(config) => !config.is_enabled(),
167 }
168}
169
170fn skip_metrics(boundary: &ErrorBoundary<Metrics>) -> bool {
171 match boundary {
172 ErrorBoundary::Err(_) => true,
173 ErrorBoundary::Ok(metrics) => metrics.is_empty(),
174 }
175}
176
177#[allow(missing_docs)]
181#[derive(Debug, Serialize)]
182#[serde(rename_all = "camelCase", remote = "ProjectConfig")]
183pub struct LimitedProjectConfig {
184 pub allowed_domains: Vec<String>,
185 pub trusted_relays: Vec<PublicKey>,
186 pub pii_config: Option<PiiConfig>,
187 #[serde(skip_serializing_if = "ProjectFiltersConfig::is_empty")]
188 pub filter_settings: ProjectFiltersConfig,
189 #[serde(skip_serializing_if = "DataScrubbingConfig::is_disabled")]
190 pub datascrubbing_settings: DataScrubbingConfig,
191 #[serde(skip_serializing_if = "Option::is_none")]
192 pub sampling: Option<ErrorBoundary<SamplingConfig>>,
193 #[serde(skip_serializing_if = "SessionMetricsConfig::is_disabled")]
194 pub session_metrics: SessionMetricsConfig,
195 #[serde(skip_serializing_if = "Option::is_none")]
196 pub transaction_metrics: Option<ErrorBoundary<TransactionMetricsConfig>>,
197 #[serde(default, skip_serializing_if = "skip_metrics_extraction")]
198 pub metric_extraction: ErrorBoundary<MetricExtractionConfig>,
199 #[serde(skip_serializing_if = "Vec::is_empty")]
200 pub metric_conditional_tagging: Vec<TaggingRule>,
201 #[serde(skip_serializing_if = "Option::is_none")]
202 pub measurements: Option<MeasurementsConfig>,
203 #[serde(skip_serializing_if = "Option::is_none")]
204 pub breakdowns_v2: Option<BreakdownsConfig>,
205 #[serde(skip_serializing_if = "Option::is_none")]
206 pub performance_score: Option<PerformanceScoreConfig>,
207 #[serde(skip_serializing_if = "FeatureSet::is_empty")]
208 pub features: FeatureSet,
209 #[serde(skip_serializing_if = "Vec::is_empty")]
210 pub tx_name_rules: Vec<TransactionNameRule>,
211 #[serde(skip_serializing_if = "is_false")]
213 pub tx_name_ready: bool,
214 #[serde(skip_serializing_if = "Option::is_none")]
219 pub span_description_rules: Option<Vec<SpanDescriptionRule>>,
220}
221
222fn is_false(value: &bool) -> bool {
223 !*value
224}
225
226#[cfg(test)]
227mod tests {
228 use crate::Feature;
229
230 use super::*;
231
232 #[test]
233 fn graduated_feature_flag_gets_inserted() {
234 let mut project_config = ProjectConfig::default();
235 assert!(!project_config.features.has(Feature::UserReportV2Ingest));
236 project_config.sanitize();
237 assert!(project_config.features.has(Feature::UserReportV2Ingest));
238 }
239}