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