relay_server/services/processor/
profile.rs

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