relay_server/services/processor/
profile.rs

1//! Profiles related processor code.
2
3use 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        // First profile found in the envelope, we'll keep it if metadata are valid.
31        ItemType::Profile if profile_id.is_none() => {
32            if profiling_disabled {
33                return ItemAction::DropSilently;
34            }
35
36            // Drop profile without a transaction in the same envelope,
37            // except if unsampled profiles are allowed for this project.
38            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        // We found another profile, we'll drop it.
54        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        // Add a valid transaction item.
137        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        // Add a profile to the same envelope.
165        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        // Setup
223        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        // Add a valid transaction item.
231        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        // Add a profile to the same envelope.
259        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        // Setup
315        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        // Add a profile to the same envelope.
323        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        // Add a valid transaction item.
353        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}