Skip to main content

relay_server/services/projects/cache/
project.rs

1use std::marker::PhantomData;
2use std::sync::Arc;
3
4use relay_config::ConfigSnapshot;
5use relay_quotas::{CachedRateLimits, DataCategory, MetricNamespaceScoping, RateLimits};
6
7use crate::Envelope;
8use crate::envelope::ItemType;
9use crate::managed::{Managed, Rejected};
10use crate::services::outcome::{DiscardReason, Outcome};
11use crate::services::projects::cache::state::SharedProject;
12use crate::services::projects::project::ProjectState;
13use crate::utils::{CheckLimits, EnvelopeLimiter};
14
15/// A loaded project.
16pub struct Project<'a> {
17    shared: SharedProject,
18    config: ConfigSnapshot,
19    // This lifetime is a leftover from before we started introducing a reloadable
20    // configuration. It's not yet removed to keep changes a bit more isolated to config.
21    //
22    // This will be removed in a follow-up PR. I promise.
23    _lifetime: PhantomData<&'a ()>,
24}
25
26impl<'a> Project<'a> {
27    pub(crate) fn new(shared: SharedProject, config: ConfigSnapshot) -> Self {
28        Self {
29            shared,
30            config,
31            _lifetime: PhantomData,
32        }
33    }
34
35    /// Returns a reference to the currently cached project state.
36    pub fn state(&self) -> &ProjectState {
37        self.shared.project_state()
38    }
39
40    /// Returns a reference to the currently cached rate limits.
41    pub fn rate_limits(&self) -> &CachedRateLimits {
42        self.shared.cached_rate_limits()
43    }
44
45    /// Checks the envelope against project configuration and rate limits.
46    ///
47    /// When `fetched`, then the project state is ensured to be up to date. When `cached`, an outdated
48    /// project state may be used, or otherwise the envelope is passed through unaltered.
49    ///
50    /// To check the envelope, this runs:
51    ///  - Validate origins and public keys
52    ///  - Quotas with a limit of `0`
53    ///  - Cached rate limits
54    pub async fn check_envelope(
55        &self,
56        envelope: &mut Managed<Box<Envelope>>,
57    ) -> Result<RateLimits, Rejected<DiscardReason>> {
58        let state = match self.state() {
59            ProjectState::Enabled(state) => Some(Arc::clone(state)),
60            ProjectState::Dummy => None,
61            ProjectState::Disabled => {
62                // TODO(jjbayer): We should refactor this function to either return a Result or
63                // handle envelope rejections internally, but not both.
64                let err = envelope
65                    .reject_err(Outcome::Invalid(DiscardReason::ProjectId))
66                    .map(|_| DiscardReason::ProjectId);
67                return Err(err);
68            }
69            ProjectState::Pending => None,
70        };
71
72        let mut scoping = envelope.scoping();
73
74        if let Some(ref state) = state {
75            scoping = state.scope_request(envelope.meta());
76            envelope.scope(scoping);
77
78            if let Err(reason) = state.check_envelope(envelope, &self.config) {
79                return Err(envelope
80                    .reject_err(Outcome::Invalid(reason))
81                    .map(|_| reason));
82            }
83        }
84
85        let current_limits = self.rate_limits().current_limits();
86
87        let quotas = state.as_deref().map(|s| s.get_quotas()).unwrap_or(&[]);
88
89        // To get the correct span outcomes, we have to partially parse the event payload
90        // and count the spans contained in the transaction events.
91        // For performance reasons, we only do this if there is an active limit on `Transaction`.
92        if current_limits
93            .is_any_limited_with_quotas(quotas, &[scoping.item(DataCategory::Transaction)])
94        {
95            ensure_span_count(envelope);
96        }
97
98        let envelope_limiter = EnvelopeLimiter::new(CheckLimits::NonIndexed, |item_scoping, _| {
99            let current_limits = Arc::clone(&current_limits);
100            async move { Ok(current_limits.check_with_quotas(quotas, item_scoping)) }
101        });
102
103        let (enforcement, mut rate_limits) = envelope_limiter.compute(envelope, &scoping).await?;
104
105        enforcement.apply_to_managed(envelope);
106
107        // Special case: Expose active rate limits for all metric namespaces if there is at least
108        // one metrics item in the Envelope to communicate backoff to SDKs. This is necessary
109        // because `EnvelopeLimiter` cannot not check metrics without parsing item contents.
110        if envelope.items().any(|i| i.ty().is_metrics()) {
111            let mut metrics_scoping = scoping.item(DataCategory::MetricBucket);
112            metrics_scoping.namespace = MetricNamespaceScoping::Any;
113            rate_limits.merge(current_limits.check_with_quotas(quotas, metrics_scoping));
114        }
115
116        Ok(rate_limits)
117    }
118}
119
120fn ensure_span_count(envelope: &mut Managed<Box<Envelope>>) {
121    envelope.modify(|envelope, records| {
122        if let Some(transaction_item) = envelope
123            .items_mut()
124            .find(|item| *item.ty() == ItemType::Transaction)
125        {
126            // We're actively 'correcting' span counts -> there will be differences.
127            records.lenient(DataCategory::Span);
128            records.lenient(DataCategory::SpanIndexed);
129            transaction_item.ensure_span_count();
130        }
131    });
132}
133
134#[cfg(test)]
135mod tests {
136    use crate::envelope::{ContentType, Envelope, Item};
137    use crate::extractors::RequestMeta;
138    use crate::services::projects::project::{ProjectInfo, PublicKeyConfig};
139    use relay_base_schema::project::{ProjectId, ProjectKey};
140    use relay_event_schema::protocol::EventId;
141    use serde_json::json;
142    use smallvec::smallvec;
143
144    use super::*;
145
146    fn create_project(config: ConfigSnapshot, data: Option<serde_json::Value>) -> Project<'static> {
147        let mut project_info = ProjectInfo {
148            project_id: Some(ProjectId::new(42)),
149            ..Default::default()
150        };
151        project_info.public_keys = smallvec![PublicKeyConfig {
152            public_key: ProjectKey::parse("e12d836b15bb49d7bbf99e64295d995b").unwrap(),
153            numeric_id: None,
154        }];
155
156        if let Some(data) = data {
157            project_info.config = serde_json::from_value(data).unwrap();
158        }
159
160        Project::new(
161            SharedProject::for_test(ProjectState::Enabled(project_info.into())),
162            config,
163        )
164    }
165
166    fn request_meta() -> RequestMeta {
167        let dsn = "https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42"
168            .parse()
169            .unwrap();
170
171        RequestMeta::new(dsn)
172    }
173
174    fn get_span_count(envelope: &Envelope) -> usize {
175        envelope.items().next().unwrap().span_count() as usize
176    }
177
178    #[tokio::test]
179    async fn test_track_nested_spans_outcomes() {
180        let config = relay_config::Config::default().current();
181        let project = create_project(
182            config,
183            Some(json!({
184                "quotas": [{
185                   "id": "foo",
186                   "categories": ["transaction"],
187                   "window": 3600,
188                   "limit": 0,
189                   "reasonCode": "foo",
190               }]
191            })),
192        );
193
194        let mut envelope = Envelope::from_request(Some(EventId::new()), request_meta());
195
196        let mut transaction = Item::new(ItemType::Transaction);
197        transaction.set_payload(
198            ContentType::Json,
199            r#"{
200  "event_id": "52df9022835246eeb317dbd739ccd059",
201  "type": "transaction",
202  "transaction": "I have a stale timestamp, but I'm recent!",
203  "start_timestamp": 1,
204  "timestamp": 2,
205  "contexts": {
206    "trace": {
207      "trace_id": "ff62a8b040f340bda5d830223def1d81",
208      "span_id": "bd429c44b67a3eb4"
209    }
210  },
211  "spans": [
212    {
213      "span_id": "bd429c44b67a3eb4",
214      "start_timestamp": 1,
215      "timestamp": null,
216      "trace_id": "ff62a8b040f340bda5d830223def1d81"
217    },
218    {
219      "span_id": "bd429c44b67a3eb5",
220      "start_timestamp": 1,
221      "timestamp": null,
222      "trace_id": "ff62a8b040f340bda5d830223def1d81"
223    }
224  ]
225}"#,
226        );
227
228        envelope.add_item(transaction);
229
230        let (outcome_aggregator, mut outcome_aggregator_rx) = relay_system::Addr::custom();
231
232        let mut managed_envelope = Managed::from_envelope(envelope, outcome_aggregator);
233
234        assert_eq!(get_span_count(&managed_envelope), 0); // not written yet
235        project.check_envelope(&mut managed_envelope).await.unwrap();
236
237        let expected = [
238            (DataCategory::Transaction, 1),
239            (DataCategory::TransactionIndexed, 1),
240            (DataCategory::Span, 3),
241            (DataCategory::SpanIndexed, 3),
242        ];
243
244        for (expected_category, expected_quantity) in expected {
245            let outcome = outcome_aggregator_rx.recv().await.unwrap();
246            assert_eq!(outcome.category, expected_category);
247            assert_eq!(outcome.quantity, expected_quantity);
248        }
249    }
250
251    #[tokio::test]
252    async fn test_track_nested_spans_outcomes_predefined() {
253        let config = relay_config::Config::default().current();
254        let project = create_project(
255            config,
256            Some(json!({
257                "quotas": [{
258                   "id": "foo",
259                   "categories": ["transaction"],
260                   "window": 3600,
261                   "limit": 0,
262                   "reasonCode": "foo",
263               }]
264            })),
265        );
266
267        let mut envelope = Envelope::from_request(Some(EventId::new()), request_meta());
268
269        let mut transaction = Item::new(ItemType::Transaction);
270        transaction.set_span_count(Some(666));
271        transaction.set_payload(
272            ContentType::Json,
273            r#"{
274  "event_id": "52df9022835246eeb317dbd739ccd059",
275  "type": "transaction",
276  "transaction": "I have a stale timestamp, but I'm recent!",
277  "start_timestamp": 1,
278  "timestamp": 2,
279  "contexts": {
280    "trace": {
281      "trace_id": "ff62a8b040f340bda5d830223def1d81",
282      "span_id": "bd429c44b67a3eb4"
283    }
284  },
285  "spans": []
286}"#,
287        );
288
289        envelope.add_item(transaction);
290
291        let (outcome_aggregator, mut outcome_aggregator_rx) = relay_system::Addr::custom();
292
293        let mut managed_envelope = Managed::from_envelope(envelope, outcome_aggregator);
294
295        assert_eq!(get_span_count(&managed_envelope), 666);
296        project.check_envelope(&mut managed_envelope).await.unwrap();
297
298        let expected = [
299            (DataCategory::Transaction, 1),
300            (DataCategory::TransactionIndexed, 1),
301            (DataCategory::Span, 667),
302            (DataCategory::SpanIndexed, 667),
303        ];
304
305        for (expected_category, expected_quantity) in expected {
306            let outcome = outcome_aggregator_rx.recv().await.unwrap();
307            assert_eq!(outcome.category, expected_category);
308            assert_eq!(outcome.quantity, expected_quantity);
309        }
310    }
311}