relay_event_normalization/eap/
mobile.rs1use std::time::Duration;
4
5use relay_conventions::attributes::*;
6use relay_event_schema::protocol::{Attributes, DeviceClass};
7use relay_protocol::Annotated;
8
9use crate::normalize::utils::{MAIN_THREAD_NAME, MAX_DURATION_MOBILE_MS, MOBILE_SDKS};
10
11pub fn normalize_mobile_attributes(attributes: &mut Annotated<Attributes>) {
19 let Some(attrs) = attributes.value_mut() else {
20 return;
21 };
22
23 if let Some(sdk_name) = attrs.get_value(SENTRY__SDK__NAME).and_then(|v| v.as_str())
24 && MOBILE_SDKS.contains(&sdk_name)
25 {
26 attrs.insert(SENTRY__MOBILE, "true".to_owned());
27
28 if let Some(thread_name) = attrs.get_value(THREAD__NAME).and_then(|v| v.as_str())
29 && thread_name == MAIN_THREAD_NAME
30 {
31 attrs.insert(SENTRY__MAIN_THREAD, "true".to_owned());
32 }
33 }
34
35 for key in [
36 APP__VITALS__START__COLD__VALUE,
37 APP__VITALS__START__WARM__VALUE,
38 APP__VITALS__START__VALUE,
39 APP__VITALS__TTID__VALUE,
40 APP__VITALS__TTFD__VALUE,
41 ] {
42 if let Some(value) = attrs.get_value(key).and_then(|v| v.as_f64())
43 && value > MAX_DURATION_MOBILE_MS
44 {
45 attrs.remove(key);
46 }
47 }
48
49 if !attrs.contains_key(APP__VITALS__START__VALUE) {
54 if let Some(value) = attrs.get_value("app_start_cold").and_then(|v| v.as_f64())
55 && value <= MAX_DURATION_MOBILE_MS
56 {
57 attrs.insert(APP__VITALS__START__VALUE, value);
58 attrs.insert_if_missing(APP__VITALS__START__TYPE, || "cold".to_owned());
59 } else if let Some(value) = attrs.get_value("app_start_warm").and_then(|v| v.as_f64())
60 && value <= MAX_DURATION_MOBILE_MS
61 {
62 attrs.insert(APP__VITALS__START__VALUE, value);
63 attrs.insert_if_missing(APP__VITALS__START__TYPE, || "warm".to_owned());
64 }
65 }
66
67 if !attrs.contains_key(DEVICE__CLASS)
69 && let Some(device_class) = DeviceClass::from_attributes(attrs)
70 {
71 attrs.insert(DEVICE__CLASS, device_class.to_string());
72 }
73}
74
75pub fn normalize_mobile_measurements(
83 attributes: &mut Annotated<Attributes>,
84 span_duration: Option<Duration>,
85) {
86 let Some(attributes) = attributes.value_mut() else {
87 return;
88 };
89
90 if let Some(frames_total) = attributes
91 .get_value(APP__VITALS__FRAMES__TOTAL__COUNT)
92 .and_then(|v| v.as_f64())
93 && frames_total > 0.0
94 {
95 if let Some(frames_frozen) = attributes
96 .get_value(APP__VITALS__FRAMES__FROZEN__COUNT)
97 .and_then(|v| v.as_f64())
98 {
99 let frames_frozen_rate = frames_frozen / frames_total;
100 attributes.insert(FRAMES_FROZEN_RATE.to_owned(), frames_frozen_rate);
101 }
102
103 if let Some(frames_slow) = attributes
104 .get_value(APP__VITALS__FRAMES__SLOW__COUNT)
105 .and_then(|v| v.as_f64())
106 {
107 let frames_slow_rate = frames_slow / frames_total;
108 attributes.insert(FRAMES_SLOW_RATE.to_owned(), frames_slow_rate);
109 }
110 }
111
112 if let Some(span_duration) = span_duration
114 && !span_duration.is_zero()
115 && let Some(stall_total_time_ms) = attributes
116 .get_value(STALL_TOTAL_TIME)
117 .and_then(|v| v.as_f64())
118 {
119 let stall_percentage = stall_total_time_ms / (span_duration.as_millis() as f64);
120 attributes.insert(STALL_PERCENTAGE.to_owned(), stall_percentage);
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use relay_protocol::assert_annotated_snapshot;
127
128 use super::*;
129
130 macro_rules! attributes {
131 ($($key:expr => $value:expr),* $(,)?) => {
132 Attributes::from([
133 $(($key.into(), Annotated::new($value.into())),)*
134 ])
135 };
136 }
137
138 macro_rules! mobile_sdk_test {
139 ($name:ident, $sdk:expr) => {
140 #[test]
141 fn $name() {
142 let mut attributes = Annotated::new(attributes! {
143 SENTRY__SDK__NAME => $sdk,
144 });
145 normalize_mobile_attributes(&mut attributes);
146 assert_annotated_snapshot!(attributes);
147 }
148 };
149 }
150
151 mobile_sdk_test!(test_mobile_tag_cocoa, "sentry.cocoa");
152 mobile_sdk_test!(test_mobile_tag_flutter, "sentry.dart.flutter");
153 mobile_sdk_test!(test_mobile_tag_android, "sentry.java.android");
154 mobile_sdk_test!(
155 test_mobile_tag_react_native,
156 "sentry.javascript.react-native"
157 );
158
159 #[test]
160 fn test_mobile_tag_not_mobile_sdk() {
161 let mut attributes = Annotated::new(attributes! {
162 SENTRY__SDK__NAME => "sentry.python",
163 });
164
165 normalize_mobile_attributes(&mut attributes);
166
167 assert_annotated_snapshot!(attributes, @r#"
168 {
169 "sentry.sdk.name": {
170 "type": "string",
171 "value": "sentry.python"
172 }
173 }
174 "#);
175 }
176
177 #[test]
178 fn test_main_thread_tag_mobile_sdk() {
179 let mut attributes = Annotated::new(attributes! {
180 SENTRY__SDK__NAME => "sentry.cocoa",
181 THREAD__NAME => "main",
182 });
183
184 normalize_mobile_attributes(&mut attributes);
185
186 assert_annotated_snapshot!(attributes, @r#"
187 {
188 "sentry.main_thread": {
189 "type": "string",
190 "value": "true"
191 },
192 "sentry.mobile": {
193 "type": "string",
194 "value": "true"
195 },
196 "sentry.sdk.name": {
197 "type": "string",
198 "value": "sentry.cocoa"
199 },
200 "thread.name": {
201 "type": "string",
202 "value": "main"
203 }
204 }
205 "#);
206 }
207
208 #[test]
209 fn test_main_thread_tag_not_main() {
210 let mut attributes = Annotated::new(attributes! {
211 SENTRY__SDK__NAME => "sentry.cocoa",
212 THREAD__NAME => "background",
213 });
214
215 normalize_mobile_attributes(&mut attributes);
216
217 assert_annotated_snapshot!(attributes, @r#"
218 {
219 "sentry.mobile": {
220 "type": "string",
221 "value": "true"
222 },
223 "sentry.sdk.name": {
224 "type": "string",
225 "value": "sentry.cocoa"
226 },
227 "thread.name": {
228 "type": "string",
229 "value": "background"
230 }
231 }
232 "#);
233 }
234
235 #[test]
236 fn test_main_thread_tag_not_set_for_non_mobile_sdk() {
237 let mut attributes = Annotated::new(attributes! {
238 SENTRY__SDK__NAME => "sentry.python",
239 THREAD__NAME => "main",
240 });
241
242 normalize_mobile_attributes(&mut attributes);
243
244 assert_annotated_snapshot!(attributes, @r#"
245 {
246 "sentry.sdk.name": {
247 "type": "string",
248 "value": "sentry.python"
249 },
250 "thread.name": {
251 "type": "string",
252 "value": "main"
253 }
254 }
255 "#);
256 }
257
258 macro_rules! outlier_test {
259 ($name:ident, $key:expr, $value:expr) => {
260 #[test]
261 fn $name() {
262 let mut attributes = Annotated::new(attributes! {
263 $key => $value,
264 });
265 normalize_mobile_attributes(&mut attributes);
266 assert_annotated_snapshot!(attributes);
267 }
268 };
269 }
270
271 outlier_test!(
272 test_outlier_removes_start_cold,
273 APP__VITALS__START__COLD__VALUE,
274 200_000.0
275 );
276 outlier_test!(
277 test_outlier_removes_start_warm,
278 APP__VITALS__START__WARM__VALUE,
279 200_000.0
280 );
281 outlier_test!(
282 test_outlier_removes_start_value,
283 APP__VITALS__START__VALUE,
284 200_000.0
285 );
286 outlier_test!(
287 test_outlier_removes_ttid,
288 APP__VITALS__TTID__VALUE,
289 200_000.0
290 );
291 outlier_test!(
292 test_outlier_removes_ttfd,
293 APP__VITALS__TTFD__VALUE,
294 200_000.0
295 );
296
297 outlier_test!(
298 test_outlier_keeps_start_cold,
299 APP__VITALS__START__COLD__VALUE,
300 5000.0
301 );
302 outlier_test!(
303 test_outlier_keeps_start_warm,
304 APP__VITALS__START__WARM__VALUE,
305 5000.0
306 );
307 outlier_test!(
308 test_outlier_keeps_start_value,
309 APP__VITALS__START__VALUE,
310 5000.0
311 );
312 outlier_test!(test_outlier_keeps_ttid, APP__VITALS__TTID__VALUE, 5000.0);
313 outlier_test!(test_outlier_keeps_ttfd, APP__VITALS__TTFD__VALUE, 5000.0);
314
315 #[test]
316 fn test_app_start_cold_normalized() {
317 let mut attributes = Annotated::new(attributes! {
318 "app_start_cold" => 1234.0,
319 });
320
321 normalize_mobile_attributes(&mut attributes);
322
323 assert_annotated_snapshot!(attributes, @r#"
324 {
325 "app.vitals.start.type": {
326 "type": "string",
327 "value": "cold"
328 },
329 "app.vitals.start.value": {
330 "type": "double",
331 "value": 1234.0
332 },
333 "app_start_cold": {
334 "type": "double",
335 "value": 1234.0
336 }
337 }
338 "#);
339 }
340
341 #[test]
342 fn test_app_start_warm_normalized() {
343 let mut attributes = Annotated::new(attributes! {
344 "app_start_warm" => 567.0,
345 });
346
347 normalize_mobile_attributes(&mut attributes);
348
349 assert_annotated_snapshot!(attributes, @r#"
350 {
351 "app.vitals.start.type": {
352 "type": "string",
353 "value": "warm"
354 },
355 "app.vitals.start.value": {
356 "type": "double",
357 "value": 567.0
358 },
359 "app_start_warm": {
360 "type": "double",
361 "value": 567.0
362 }
363 }
364 "#);
365 }
366
367 #[test]
368 fn test_app_start_v2_not_overwritten() {
369 let mut attributes = Annotated::new(attributes! {
370 APP__VITALS__START__VALUE => 999.0,
371 APP__VITALS__START__TYPE => "warm",
372 "app_start_cold" => 1234.0,
373 });
374
375 normalize_mobile_attributes(&mut attributes);
376
377 assert_annotated_snapshot!(attributes, @r#"
378 {
379 "app.vitals.start.type": {
380 "type": "string",
381 "value": "warm"
382 },
383 "app.vitals.start.value": {
384 "type": "double",
385 "value": 999.0
386 },
387 "app_start_cold": {
388 "type": "double",
389 "value": 1234.0
390 }
391 }
392 "#);
393 }
394
395 #[test]
396 fn test_device_class_iphone() {
397 let mut attributes = Annotated::new(attributes! {
398 DEVICE__FAMILY => "iPhone",
399 DEVICE__MODEL => "iPhone17,5",
400 });
401
402 normalize_mobile_attributes(&mut attributes);
403
404 assert_annotated_snapshot!(attributes, @r#"
405 {
406 "device.class": {
407 "type": "string",
408 "value": "3"
409 },
410 "device.family": {
411 "type": "string",
412 "value": "iPhone"
413 },
414 "device.model": {
415 "type": "string",
416 "value": "iPhone17,5"
417 }
418 }
419 "#);
420 }
421
422 #[test]
423 fn test_device_class_android() {
424 let mut attributes = Annotated::new(attributes! {
425 DEVICE__FAMILY => "Android",
426 DEVICE__PROCESSOR_FREQUENCY => 3000.0,
427 DEVICE__PROCESSOR_COUNT => 8.0,
428 DEVICE__MEMORY_SIZE => 8_589_934_592.0,
429 });
430
431 normalize_mobile_attributes(&mut attributes);
432
433 assert_annotated_snapshot!(attributes, @r#"
434 {
435 "device.class": {
436 "type": "string",
437 "value": "3"
438 },
439 "device.family": {
440 "type": "string",
441 "value": "Android"
442 },
443 "device.memory_size": {
444 "type": "double",
445 "value": 8589934592.0
446 },
447 "device.processor_count": {
448 "type": "double",
449 "value": 8.0
450 },
451 "device.processor_frequency": {
452 "type": "double",
453 "value": 3000.0
454 }
455 }
456 "#);
457 }
458
459 #[test]
460 fn test_device_class_missing_attrs() {
461 let mut attributes = Annotated::new(attributes! {
462 DEVICE__FAMILY => "Android",
463 });
464
465 normalize_mobile_attributes(&mut attributes);
466
467 assert_annotated_snapshot!(attributes, @r#"
468 {
469 "device.family": {
470 "type": "string",
471 "value": "Android"
472 }
473 }
474 "#);
475 }
476}