relay_server/services/processor/
profile.rs1use relay_dynamic_config::Feature;
4
5use relay_base_schema::events::EventType;
6use relay_base_schema::project::ProjectId;
7use relay_config::Config;
8use relay_event_schema::protocol::Event;
9use relay_profiling::{ProfileError, ProfileId};
10use relay_protocol::Annotated;
11
12use crate::envelope::ItemType;
13use crate::managed::{ItemAction, TypedEnvelope};
14use crate::processing::utils::event::event_type;
15use crate::services::outcome::{DiscardReason, Outcome};
16use crate::services::processor::should_filter;
17use crate::services::projects::project::ProjectInfo;
18
19pub fn filter<Group>(
20 managed_envelope: &mut TypedEnvelope<Group>,
21 event: &Annotated<Event>,
22 config: &Config,
23 project_id: ProjectId,
24 project_info: &ProjectInfo,
25) -> Option<ProfileId> {
26 let profiling_disabled = should_filter(config, project_info, Feature::Profiling);
27 let has_transaction = event_type(event) == Some(EventType::Transaction);
28 let mut profile_id = None;
29 managed_envelope.retain_items(|item| match item.ty() {
30 ItemType::Profile if profile_id.is_none() => {
32 if profiling_disabled {
33 return ItemAction::DropSilently;
34 }
35
36 let profile_allowed = has_transaction || !item.sampled();
39 if !profile_allowed {
40 return ItemAction::DropSilently;
41 }
42
43 match relay_profiling::parse_metadata(&item.payload(), project_id) {
44 Ok(id) => {
45 profile_id = Some(id);
46 ItemAction::Keep
47 }
48 Err(err) => ItemAction::Drop(Outcome::Invalid(DiscardReason::Profiling(
49 relay_profiling::discard_reason(&err),
50 ))),
51 }
52 }
53 ItemType::Profile => ItemAction::Drop(Outcome::Invalid(DiscardReason::Profiling(
55 relay_profiling::discard_reason(&ProfileError::TooManyProfiles),
56 ))),
57 _ => ItemAction::Keep,
58 });
59
60 profile_id
61}
62
63#[cfg(test)]
64mod tests {
65 use crate::envelope::{ContentType, Envelope, Item};
66 use crate::extractors::RequestMeta;
67 use crate::managed::ManagedEnvelope;
68 use crate::processing::{self, Outputs};
69 use crate::services::processor::Submit;
70 use crate::services::processor::{ProcessEnvelopeGrouped, ProcessingGroup};
71 use crate::services::projects::project::ProjectInfo;
72 use crate::testutils::create_test_processor;
73 use insta::assert_debug_snapshot;
74 use relay_dynamic_config::{ErrorBoundary, Feature, GlobalConfig, TransactionMetricsConfig};
75 use relay_event_schema::protocol::{EventId, ProfileContext};
76 use relay_system::Addr;
77
78 use super::*;
79
80 async fn process_event(envelope: Box<Envelope>) -> Option<Annotated<Event>> {
81 let config = Config::from_json_value(serde_json::json!({
82 "processing": {
83 "enabled": true,
84 "kafka_config": []
85 }
86 }))
87 .unwrap();
88 let processor = create_test_processor(config).await;
89 let mut envelopes = ProcessingGroup::split_envelope(*envelope, &Default::default());
90 assert_eq!(envelopes.len(), 1);
91 let (group, envelope) = envelopes.pop().unwrap();
92
93 let envelope = ManagedEnvelope::new(envelope, Addr::dummy());
94
95 let mut project_info = ProjectInfo::default().sanitized(false);
96 project_info.config.transaction_metrics =
97 Some(ErrorBoundary::Ok(TransactionMetricsConfig::new()));
98 project_info.config.features.0.insert(Feature::Profiling);
99
100 let mut global_config = GlobalConfig::default();
101 global_config.normalize();
102 let message = ProcessEnvelopeGrouped {
103 group,
104 envelope,
105 ctx: processing::Context {
106 config: &processor.inner.config,
107 project_info: &project_info,
108 global_config: &global_config,
109 ..processing::Context::for_test()
110 },
111 };
112
113 let result = processor.process(message).await.unwrap()?;
114
115 let Submit::Output {
116 output: Outputs::Transactions(t),
117 ctx: _,
118 } = result
119 else {
120 panic!();
121 };
122 Some(t.event().unwrap())
123 }
124
125 #[tokio::test]
126 async fn test_profile_id_transfered() {
127 relay_log::init_test!();
128
129 let event_id = EventId::new();
130 let dsn = "https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42"
131 .parse()
132 .unwrap();
133 let request_meta = RequestMeta::new(dsn);
134 let mut envelope = Envelope::from_request(Some(event_id), request_meta);
135
136 envelope.add_item({
138 let mut item = Item::new(ItemType::Transaction);
139
140 item.set_payload(
141 ContentType::Json,
142 r#"{
143 "event_id": "9b73438f70e044ecbd006b7fd15b7373",
144 "type": "transaction",
145 "transaction": "/foo/",
146 "timestamp": 946684810.0,
147 "start_timestamp": 946684800.0,
148 "contexts": {
149 "trace": {
150 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
151 "span_id": "fa90fdead5f74053",
152 "op": "http.server",
153 "type": "trace"
154 }
155 },
156 "transaction_info": {
157 "source": "url"
158 }
159 }"#,
160 );
161 item
162 });
163
164 envelope.add_item({
166 let mut item = Item::new(ItemType::Profile);
167 item.set_payload(
168 ContentType::Json,
169 r#"{
170 "profile_id": "012d836b15bb49d7bbf99e64295d995b",
171 "version": "1",
172 "platform": "android",
173 "os": {"name": "foo", "version": "bar"},
174 "device": {"architecture": "zap"},
175 "timestamp": "2023-10-10 00:00:00Z",
176 "profile": {
177 "samples":[
178 {
179 "stack_id":0,
180 "elapsed_since_start_ns":1,
181 "thread_id":1
182 },
183 {
184 "stack_id":0,
185 "elapsed_since_start_ns":2,
186 "thread_id":1
187 }
188 ],
189 "stacks":[[0]],
190 "frames":[{
191 "function":"main"
192 }]
193 },
194 "transactions": [
195 {
196 "id": "9b73438f70e044ecbd006b7fd15b7373",
197 "name": "/foo/",
198 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2"
199 }
200 ]
201 }"#,
202 );
203 item
204 });
205
206 let event = process_event(envelope).await.unwrap();
207
208 let context = event.value().unwrap().context::<ProfileContext>().unwrap();
209
210 assert_debug_snapshot!(context, @r###"
211 ProfileContext {
212 profile_id: EventId(
213 012d836b-15bb-49d7-bbf9-9e64295d995b,
214 ),
215 profiler_id: ~,
216 }
217 "###);
218 }
219
220 #[tokio::test]
221 async fn test_invalid_profile_id_not_transfered() {
222 let event_id = EventId::new();
224 let dsn = "https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42"
225 .parse()
226 .unwrap();
227 let request_meta = RequestMeta::new(dsn);
228 let mut envelope = Envelope::from_request(Some(event_id), request_meta);
229
230 envelope.add_item({
232 let mut item = Item::new(ItemType::Transaction);
233
234 item.set_payload(
235 ContentType::Json,
236 r#"{
237 "event_id": "9b73438f70e044ecbd006b7fd15b7373",
238 "type": "transaction",
239 "transaction": "/foo/",
240 "timestamp": 946684810.0,
241 "start_timestamp": 946684800.0,
242 "contexts": {
243 "trace": {
244 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
245 "span_id": "fa90fdead5f74053",
246 "op": "http.server",
247 "type": "trace"
248 }
249 },
250 "transaction_info": {
251 "source": "url"
252 }
253 }"#,
254 );
255 item
256 });
257
258 envelope.add_item({
260 let mut item = Item::new(ItemType::Profile);
261 item.set_payload(
262 ContentType::Json,
263 r#"{
264 "profile_id": "012d836b15bb49d7bbf99e64295d995b",
265 "version": "1",
266 "platform": "android",
267 "os": {"name": "foo", "version": "bar"},
268 "device": {"architecture": "zap"},
269 "timestamp": "2023-10-10 00:00:00Z",
270 "profile": {
271 "samples":[
272 {
273 "stack_id":0,
274 "elapsed_since_start_ns":1,
275 "thread_id":1
276 },
277 {
278 "stack_id":1,
279 "elapsed_since_start_ns":2,
280 "thread_id":1
281 }
282 ],
283 "stacks":[[0],[]],
284 "frames":[{
285 "function":"main"
286 }]
287 },
288 "transactions": [
289 {
290 "id": "9b73438f70e044ecbd006b7fd15b7373",
291 "name": "/foo/",
292 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2"
293 }
294 ]
295 }"#,
296 );
297 item
298 });
299
300 let event = process_event(envelope).await.unwrap();
301 let context = event.value().unwrap().context::<ProfileContext>().unwrap();
302
303 assert_debug_snapshot!(context, @r###"
304 ProfileContext {
305 profile_id: ~,
306 profiler_id: ~,
307 }
308 "###);
309 }
310
311 #[tokio::test]
312 async fn filter_standalone_profile() {
313 relay_log::init_test!();
314 let event_id = EventId::new();
316 let dsn = "https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42"
317 .parse()
318 .unwrap();
319 let request_meta = RequestMeta::new(dsn);
320 let mut envelope = Envelope::from_request(Some(event_id), request_meta);
321
322 envelope.add_item({
324 let mut item = Item::new(ItemType::Profile);
325 item.set_payload(
326 ContentType::Json,
327 r#"{
328 "profile_id": "012d836b15bb49d7bbf99e64295d995b",
329 "version": "1",
330 "platform": "android",
331 "os": {"name": "foo", "version": "bar"},
332 "device": {"architecture": "zap"},
333 "timestamp": "2023-10-10 00:00:00Z"
334 }"#,
335 );
336 item
337 });
338
339 let event = process_event(envelope).await;
340 assert!(event.is_none());
341 }
342
343 #[tokio::test]
344 async fn test_profile_id_removed_profiler_id_kept() {
345 let event_id = EventId::new();
346 let dsn = "https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42"
347 .parse()
348 .unwrap();
349 let request_meta = RequestMeta::new(dsn);
350 let mut envelope = Envelope::from_request(Some(event_id), request_meta);
351
352 envelope.add_item({
354 let mut item = Item::new(ItemType::Transaction);
355
356 item.set_payload(
357 ContentType::Json,
358 r#"{
359 "type": "transaction",
360 "transaction": "/foo/",
361 "timestamp": 946684810.0,
362 "start_timestamp": 946684800.0,
363 "contexts": {
364 "trace": {
365 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
366 "span_id": "fa90fdead5f74053",
367 "op": "http.server",
368 "type": "trace"
369 },
370 "profile": {
371 "profile_id": "4c79f60c11214eb38604f4ae0781bfb2",
372 "profiler_id": "4c79f60c11214eb38604f4ae0781bfb2",
373 "type": "profile"
374 }
375 },
376 "transaction_info": {
377 "source": "url"
378 }
379 }"#,
380 );
381 item
382 });
383
384 let mut project_info = ProjectInfo::default();
385 project_info.config.features.0.insert(Feature::Profiling);
386
387 let event = process_event(envelope).await.unwrap();
388 let context = event.value().unwrap().context::<ProfileContext>().unwrap();
389
390 assert_debug_snapshot!(context, @r###"
391 ProfileContext {
392 profile_id: ~,
393 profiler_id: EventId(
394 4c79f60c-1121-4eb3-8604-f4ae0781bfb2,
395 ),
396 }
397 "###);
398 }
399}