relay_server/services/projects/cache/
project.rs1use 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
15pub struct Project<'a> {
17 shared: SharedProject,
18 config: ConfigSnapshot,
19 _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 pub fn state(&self) -> &ProjectState {
37 self.shared.project_state()
38 }
39
40 pub fn rate_limits(&self) -> &CachedRateLimits {
42 self.shared.cached_rate_limits()
43 }
44
45 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 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 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(¤t_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 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 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); 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}