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