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