1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
use relay_auth::PublicKey;
use relay_event_normalization::{
    BreakdownsConfig, MeasurementsConfig, PerformanceScoreConfig, SpanDescriptionRule,
    TransactionNameRule,
};
use relay_filter::ProjectFiltersConfig;
use relay_pii::{DataScrubbingConfig, PiiConfig};
use relay_quotas::Quota;
use relay_sampling::SamplingConfig;
use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::error_boundary::ErrorBoundary;
use crate::feature::FeatureSet;
use crate::metrics::{
    self, MetricExtractionConfig, Metrics, SessionMetricsConfig, TaggingRule,
    TransactionMetricsConfig,
};
use crate::{defaults, GRADUATED_FEATURE_FLAGS};

/// Dynamic, per-DSN configuration passed down from Sentry.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct ProjectConfig {
    /// URLs that are permitted for cross original JavaScript requests.
    pub allowed_domains: Vec<String>,
    /// List of relay public keys that are permitted to access this project.
    pub trusted_relays: Vec<PublicKey>,
    /// Configuration for PII stripping.
    pub pii_config: Option<PiiConfig>,
    /// The grouping configuration.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub grouping_config: Option<Value>,
    /// Configuration for filter rules.
    #[serde(skip_serializing_if = "ProjectFiltersConfig::is_empty")]
    pub filter_settings: ProjectFiltersConfig,
    /// Configuration for data scrubbers.
    #[serde(skip_serializing_if = "DataScrubbingConfig::is_disabled")]
    pub datascrubbing_settings: DataScrubbingConfig,
    /// Maximum event retention for the organization.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub event_retention: Option<u16>,
    /// Usage quotas for this project.
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub quotas: Vec<Quota>,
    /// Configuration for sampling traces, if not present there will be no sampling.
    #[serde(alias = "dynamicSampling", skip_serializing_if = "Option::is_none")]
    pub sampling: Option<ErrorBoundary<SamplingConfig>>,
    /// Configuration for measurements.
    /// NOTE: do not access directly, use [`relay_event_normalization::CombinedMeasurementsConfig`].
    #[serde(skip_serializing_if = "Option::is_none")]
    pub measurements: Option<MeasurementsConfig>,
    /// Configuration for operation breakdown. Will be emitted only if present.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub breakdowns_v2: Option<BreakdownsConfig>,
    /// Configuration for performance score calculations. Will be emitted only if present.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub performance_score: Option<PerformanceScoreConfig>,
    /// Configuration for extracting metrics from sessions.
    #[serde(skip_serializing_if = "SessionMetricsConfig::is_disabled")]
    pub session_metrics: SessionMetricsConfig,
    /// Configuration for extracting metrics from transaction events.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub transaction_metrics: Option<ErrorBoundary<TransactionMetricsConfig>>,
    /// Configuration for generic metrics extraction from all data categories.
    #[serde(default, skip_serializing_if = "skip_metrics_extraction")]
    pub metric_extraction: ErrorBoundary<MetricExtractionConfig>,
    /// Rules for applying metrics tags depending on the event's content.
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub metric_conditional_tagging: Vec<TaggingRule>,
    /// Exposable features enabled for this project.
    #[serde(skip_serializing_if = "FeatureSet::is_empty")]
    pub features: FeatureSet,
    /// Transaction renaming rules.
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub tx_name_rules: Vec<TransactionNameRule>,
    /// Whether or not a project is ready to mark all URL transactions as "sanitized".
    #[serde(skip_serializing_if = "is_false")]
    pub tx_name_ready: bool,
    /// Span description renaming rules.
    ///
    /// These are currently not used by Relay, and only here to be forwarded to old
    /// relays that might still need them.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub span_description_rules: Option<Vec<SpanDescriptionRule>>,
    /// Configuration for metrics.
    #[serde(default, skip_serializing_if = "skip_metrics")]
    pub metrics: ErrorBoundary<Metrics>,
}

impl ProjectConfig {
    /// Validates fields in this project config and removes values that are partially invalid.
    pub fn sanitize(&mut self) {
        self.quotas.retain(Quota::is_valid);

        metrics::convert_conditional_tagging(self);
        defaults::add_span_metrics(self);

        if let Some(ErrorBoundary::Ok(ref mut sampling_config)) = self.sampling {
            sampling_config.normalize();
        }

        for flag in GRADUATED_FEATURE_FLAGS {
            self.features.0.insert(*flag);
        }
    }
}

impl Default for ProjectConfig {
    fn default() -> Self {
        ProjectConfig {
            allowed_domains: vec!["*".to_string()],
            trusted_relays: vec![],
            pii_config: None,
            grouping_config: None,
            filter_settings: ProjectFiltersConfig::default(),
            datascrubbing_settings: DataScrubbingConfig::default(),
            event_retention: None,
            quotas: Vec::new(),
            sampling: None,
            measurements: None,
            breakdowns_v2: None,
            performance_score: Default::default(),
            session_metrics: SessionMetricsConfig::default(),
            transaction_metrics: None,
            metric_extraction: Default::default(),
            metric_conditional_tagging: Vec::new(),
            features: Default::default(),
            tx_name_rules: Vec::new(),
            tx_name_ready: false,
            span_description_rules: None,
            metrics: Default::default(),
        }
    }
}

fn skip_metrics_extraction(boundary: &ErrorBoundary<MetricExtractionConfig>) -> bool {
    match boundary {
        ErrorBoundary::Err(_) => true,
        ErrorBoundary::Ok(config) => !config.is_enabled(),
    }
}

fn skip_metrics(boundary: &ErrorBoundary<Metrics>) -> bool {
    match boundary {
        ErrorBoundary::Err(_) => true,
        ErrorBoundary::Ok(metrics) => metrics.is_empty(),
    }
}

/// Subset of [`ProjectConfig`] that is passed to external Relays.
///
/// For documentation of the fields, see [`ProjectConfig`].
#[allow(missing_docs)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase", remote = "ProjectConfig")]
pub struct LimitedProjectConfig {
    pub allowed_domains: Vec<String>,
    pub trusted_relays: Vec<PublicKey>,
    pub pii_config: Option<PiiConfig>,
    #[serde(skip_serializing_if = "ProjectFiltersConfig::is_empty")]
    pub filter_settings: ProjectFiltersConfig,
    #[serde(skip_serializing_if = "DataScrubbingConfig::is_disabled")]
    pub datascrubbing_settings: DataScrubbingConfig,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sampling: Option<ErrorBoundary<SamplingConfig>>,
    #[serde(skip_serializing_if = "SessionMetricsConfig::is_disabled")]
    pub session_metrics: SessionMetricsConfig,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub transaction_metrics: Option<ErrorBoundary<TransactionMetricsConfig>>,
    #[serde(default, skip_serializing_if = "skip_metrics_extraction")]
    pub metric_extraction: ErrorBoundary<MetricExtractionConfig>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub metric_conditional_tagging: Vec<TaggingRule>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub measurements: Option<MeasurementsConfig>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub breakdowns_v2: Option<BreakdownsConfig>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub performance_score: Option<PerformanceScoreConfig>,
    #[serde(skip_serializing_if = "FeatureSet::is_empty")]
    pub features: FeatureSet,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub tx_name_rules: Vec<TransactionNameRule>,
    /// Whether or not a project is ready to mark all URL transactions as "sanitized".
    #[serde(skip_serializing_if = "is_false")]
    pub tx_name_ready: bool,
    /// Span description renaming rules.
    ///
    /// These are currently not used by Relay, and only here to be forwarded to old
    /// relays that might still need them.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub span_description_rules: Option<Vec<SpanDescriptionRule>>,
}

fn is_false(value: &bool) -> bool {
    !*value
}

#[cfg(test)]
mod tests {
    use crate::Feature;

    use super::*;

    #[test]
    fn graduated_feature_flag_gets_inserted() {
        let mut project_config = ProjectConfig::default();
        assert!(!project_config.features.has(Feature::UserReportV2Ingest));
        project_config.sanitize();
        assert!(project_config.features.has(Feature::UserReportV2Ingest));
    }
}