relay_event_schema/protocol/
device_class.rs

1use std::fmt;
2
3use relay_protocol::{Annotated, Empty, FromValue, IntoValue};
4
5use crate::protocol::{Contexts, DeviceContext};
6
7#[derive(Clone, Copy, Debug, FromValue, IntoValue, Empty, PartialEq)]
8pub struct DeviceClass(pub u64);
9
10const GIB: u64 = 1024 * 1024 * 1024;
11
12impl DeviceClass {
13    pub const LOW: Self = Self(1);
14    pub const MEDIUM: Self = Self(2);
15    pub const HIGH: Self = Self(3);
16
17    pub fn from_contexts(contexts: &Contexts) -> Option<DeviceClass> {
18        let device = contexts.get::<DeviceContext>()?;
19        let family = device.family.value()?;
20
21        if family == "iPhone" || family == "iOS" || family == "iOS-Device" {
22            model_to_class(device.model.as_str()?)
23        } else if let (Some(&freq), Some(&proc), Some(&mem)) = (
24            device.processor_frequency.value(),
25            device.processor_count.value(),
26            device.memory_size.value(),
27        ) {
28            if freq < 2000 || proc < 8 || mem < 4 * GIB {
29                Some(DeviceClass::LOW)
30            } else if freq < 2500 || mem < 6 * GIB {
31                Some(DeviceClass::MEDIUM)
32            } else {
33                Some(DeviceClass::HIGH)
34            }
35        } else {
36            None
37        }
38    }
39}
40
41impl fmt::Display for DeviceClass {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        self.0.fmt(f)
44    }
45}
46
47fn model_to_class(model: &str) -> Option<DeviceClass> {
48    match model {
49        // iPhones
50        "iPhone1,1" => Some(DeviceClass::LOW),
51        "iPhone1,2" => Some(DeviceClass::LOW),
52        "iPhone2,1" => Some(DeviceClass::LOW),
53        "iPhone3,1" => Some(DeviceClass::LOW),
54        "iPhone3,2" => Some(DeviceClass::LOW),
55        "iPhone3,3" => Some(DeviceClass::LOW),
56        "iPhone4,1" => Some(DeviceClass::LOW),
57        "iPhone5,1" => Some(DeviceClass::LOW),
58        "iPhone5,2" => Some(DeviceClass::LOW),
59        "iPhone5,3" => Some(DeviceClass::LOW),
60        "iPhone5,4" => Some(DeviceClass::LOW),
61        "iPhone6,1" => Some(DeviceClass::LOW),
62        "iPhone6,2" => Some(DeviceClass::LOW),
63        "iPhone7,1" => Some(DeviceClass::LOW),
64        "iPhone7,2" => Some(DeviceClass::LOW),
65        "iPhone8,1" => Some(DeviceClass::LOW),
66        "iPhone8,2" => Some(DeviceClass::LOW),
67        "iPhone8,4" => Some(DeviceClass::LOW),
68        "iPhone9,1" => Some(DeviceClass::MEDIUM),
69        "iPhone9,3" => Some(DeviceClass::MEDIUM),
70        "iPhone9,2" => Some(DeviceClass::MEDIUM),
71        "iPhone9,4" => Some(DeviceClass::MEDIUM),
72        "iPhone10,1" => Some(DeviceClass::MEDIUM),
73        "iPhone10,4" => Some(DeviceClass::MEDIUM),
74        "iPhone10,2" => Some(DeviceClass::MEDIUM),
75        "iPhone10,5" => Some(DeviceClass::MEDIUM),
76        "iPhone10,3" => Some(DeviceClass::MEDIUM),
77        "iPhone10,6" => Some(DeviceClass::MEDIUM),
78        "iPhone11,8" => Some(DeviceClass::MEDIUM),
79        "iPhone11,2" => Some(DeviceClass::MEDIUM),
80        "iPhone11,4" => Some(DeviceClass::MEDIUM),
81        "iPhone11,6" => Some(DeviceClass::MEDIUM),
82        "iPhone12,1" => Some(DeviceClass::MEDIUM),
83        "iPhone12,3" => Some(DeviceClass::MEDIUM),
84        "iPhone12,5" => Some(DeviceClass::MEDIUM),
85        "iPhone12,8" => Some(DeviceClass::MEDIUM),
86        "iPhone13,1" => Some(DeviceClass::HIGH),
87        "iPhone13,2" => Some(DeviceClass::HIGH),
88        "iPhone13,3" => Some(DeviceClass::HIGH),
89        "iPhone13,4" => Some(DeviceClass::HIGH),
90        "iPhone14,4" => Some(DeviceClass::HIGH),
91        "iPhone14,5" => Some(DeviceClass::HIGH),
92        "iPhone14,2" => Some(DeviceClass::HIGH),
93        "iPhone14,3" => Some(DeviceClass::HIGH),
94        "iPhone14,6" => Some(DeviceClass::HIGH),
95        "iPhone14,7" => Some(DeviceClass::HIGH),
96        "iPhone14,8" => Some(DeviceClass::HIGH),
97        "iPhone15,2" => Some(DeviceClass::HIGH),
98        "iPhone15,3" => Some(DeviceClass::HIGH),
99        "iPhone15,4" => Some(DeviceClass::HIGH),
100        "iPhone15,5" => Some(DeviceClass::HIGH),
101        "iPhone16,1" => Some(DeviceClass::HIGH),
102        "iPhone16,2" => Some(DeviceClass::HIGH),
103        "iPhone17,1" => Some(DeviceClass::HIGH),
104        "iPhone17,2" => Some(DeviceClass::HIGH),
105        "iPhone17,3" => Some(DeviceClass::HIGH),
106        "iPhone17,4" => Some(DeviceClass::HIGH),
107        "iPhone17,5" => Some(DeviceClass::HIGH),
108
109        // iPads
110        "iPad1,1" => Some(DeviceClass::LOW),
111        "iPad1,2" => Some(DeviceClass::LOW),
112        "iPad2,1" => Some(DeviceClass::LOW),
113        "iPad2,2" => Some(DeviceClass::LOW),
114        "iPad2,3" => Some(DeviceClass::LOW),
115        "iPad2,4" => Some(DeviceClass::LOW),
116        "iPad3,1" => Some(DeviceClass::LOW),
117        "iPad3,2" => Some(DeviceClass::LOW),
118        "iPad3,3" => Some(DeviceClass::LOW),
119        "iPad2,5" => Some(DeviceClass::LOW),
120        "iPad2,6" => Some(DeviceClass::LOW),
121        "iPad2,7" => Some(DeviceClass::LOW),
122        "iPad3,4" => Some(DeviceClass::LOW),
123        "iPad3,5" => Some(DeviceClass::LOW),
124        "iPad3,6" => Some(DeviceClass::LOW),
125        "iPad4,1" => Some(DeviceClass::LOW),
126        "iPad4,2" => Some(DeviceClass::LOW),
127        "iPad4,3" => Some(DeviceClass::LOW),
128        "iPad4,4" => Some(DeviceClass::LOW),
129        "iPad4,5" => Some(DeviceClass::LOW),
130        "iPad4,6" => Some(DeviceClass::LOW),
131        "iPad4,7" => Some(DeviceClass::LOW),
132        "iPad4,8" => Some(DeviceClass::LOW),
133        "iPad4,9" => Some(DeviceClass::LOW),
134        "iPad5,1" => Some(DeviceClass::LOW),
135        "iPad5,2" => Some(DeviceClass::LOW),
136        "iPad5,3" => Some(DeviceClass::LOW),
137        "iPad5,4" => Some(DeviceClass::LOW),
138        "iPad6,3" => Some(DeviceClass::MEDIUM),
139        "iPad6,4" => Some(DeviceClass::MEDIUM),
140        "iPad6,7" => Some(DeviceClass::MEDIUM),
141        "iPad6,8" => Some(DeviceClass::MEDIUM),
142        "iPad6,11" => Some(DeviceClass::LOW),
143        "iPad6,12" => Some(DeviceClass::LOW),
144        "iPad7,2" => Some(DeviceClass::MEDIUM),
145        "iPad7,3" => Some(DeviceClass::MEDIUM),
146        "iPad7,4" => Some(DeviceClass::MEDIUM),
147        "iPad7,5" => Some(DeviceClass::MEDIUM),
148        "iPad7,6" => Some(DeviceClass::MEDIUM),
149        "iPad7,1" => Some(DeviceClass::MEDIUM),
150        "iPad7,11" => Some(DeviceClass::MEDIUM),
151        "iPad7,12" => Some(DeviceClass::MEDIUM),
152        "iPad8,1" => Some(DeviceClass::MEDIUM),
153        "iPad8,2" => Some(DeviceClass::MEDIUM),
154        "iPad8,3" => Some(DeviceClass::MEDIUM),
155        "iPad8,4" => Some(DeviceClass::MEDIUM),
156        "iPad8,5" => Some(DeviceClass::MEDIUM),
157        "iPad8,6" => Some(DeviceClass::MEDIUM),
158        "iPad8,7" => Some(DeviceClass::MEDIUM),
159        "iPad8,8" => Some(DeviceClass::MEDIUM),
160        "iPad8,9" => Some(DeviceClass::MEDIUM),
161        "iPad8,10" => Some(DeviceClass::MEDIUM),
162        "iPad8,11" => Some(DeviceClass::MEDIUM),
163        "iPad8,12" => Some(DeviceClass::MEDIUM),
164        "iPad11,1" => Some(DeviceClass::MEDIUM),
165        "iPad11,2" => Some(DeviceClass::MEDIUM),
166        "iPad11,3" => Some(DeviceClass::MEDIUM),
167        "iPad11,4" => Some(DeviceClass::MEDIUM),
168        "iPad11,6" => Some(DeviceClass::MEDIUM),
169        "iPad11,7" => Some(DeviceClass::MEDIUM),
170        "iPad12,1" => Some(DeviceClass::MEDIUM),
171        "iPad12,2" => Some(DeviceClass::MEDIUM),
172        "iPad14,1" => Some(DeviceClass::HIGH),
173        "iPad14,2" => Some(DeviceClass::HIGH),
174        "iPad13,1" => Some(DeviceClass::HIGH),
175        "iPad13,2" => Some(DeviceClass::HIGH),
176        "iPad13,4" => Some(DeviceClass::HIGH),
177        "iPad13,5" => Some(DeviceClass::HIGH),
178        "iPad13,6" => Some(DeviceClass::HIGH),
179        "iPad13,7" => Some(DeviceClass::HIGH),
180        "iPad13,8" => Some(DeviceClass::HIGH),
181        "iPad13,9" => Some(DeviceClass::HIGH),
182        "iPad13,10" => Some(DeviceClass::HIGH),
183        "iPad13,11" => Some(DeviceClass::HIGH),
184        "iPad13,16" => Some(DeviceClass::HIGH),
185        "iPad13,17" => Some(DeviceClass::HIGH),
186        "iPad13,18" => Some(DeviceClass::HIGH),
187        "iPad13,19" => Some(DeviceClass::HIGH),
188        "iPad14,3" => Some(DeviceClass::HIGH),
189        "iPad14,4" => Some(DeviceClass::HIGH),
190        "iPad14,5" => Some(DeviceClass::HIGH),
191        "iPad14,6" => Some(DeviceClass::HIGH),
192        "iPad14,8" => Some(DeviceClass::HIGH),
193        "iPad14,9" => Some(DeviceClass::HIGH),
194        "iPad14,10" => Some(DeviceClass::HIGH),
195        "iPad14,11" => Some(DeviceClass::HIGH),
196        "iPad16,1" => Some(DeviceClass::HIGH),
197        "iPad16,2" => Some(DeviceClass::HIGH),
198        "iPad16,3" => Some(DeviceClass::HIGH),
199        "iPad16,4" => Some(DeviceClass::HIGH),
200        "iPad16,5" => Some(DeviceClass::HIGH),
201        "iPad16,6" => Some(DeviceClass::HIGH),
202
203        // If we don't know the model it's a new device and therefore must be high.
204        _ => Some(DeviceClass::HIGH),
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_iphone17_5_returns_device_class_high() {
214        let mut contexts = Contexts::new();
215        contexts.add(DeviceContext {
216            family: Annotated::new("iOS".to_string()),
217            model: Annotated::new("iPhone17,5".to_string()),
218            ..DeviceContext::default()
219        });
220        assert_eq!(
221            DeviceClass::from_contexts(&contexts),
222            Some(DeviceClass::HIGH)
223        );
224    }
225
226    #[test]
227    fn test_iphone99_1_returns_device_class_high() {
228        let mut contexts = Contexts::new();
229        contexts.add(DeviceContext {
230            family: Annotated::new("iOS".to_string()),
231            model: Annotated::new("iPhone99,1".to_string()),
232            ..DeviceContext::default()
233        });
234        assert_eq!(
235            DeviceClass::from_contexts(&contexts),
236            Some(DeviceClass::HIGH)
237        );
238    }
239
240    #[test]
241    fn test_ipad99_1_returns_device_class_high() {
242        let mut contexts = Contexts::new();
243        contexts.add(DeviceContext {
244            family: Annotated::new("iOS".to_string()),
245            model: Annotated::new("iPad99,1".to_string()),
246            ..DeviceContext::default()
247        });
248        assert_eq!(
249            DeviceClass::from_contexts(&contexts),
250            Some(DeviceClass::HIGH)
251        );
252    }
253
254    #[test]
255    fn test_garbage_device_model_returns_device_class_high() {
256        let mut contexts = Contexts::new();
257        contexts.add(DeviceContext {
258            family: Annotated::new("iOS".to_string()),
259            model: Annotated::new("garbage-device-model".to_string()),
260            ..DeviceContext::default()
261        });
262        assert_eq!(
263            DeviceClass::from_contexts(&contexts),
264            Some(DeviceClass::HIGH)
265        );
266    }
267
268    #[test]
269    fn test_wrong_family_returns_none() {
270        let mut contexts = Contexts::new();
271        contexts.add(DeviceContext {
272            family: Annotated::new("iOSS".to_string()),
273            model: Annotated::new("iPhone17,5".to_string()),
274            ..DeviceContext::default()
275        });
276        assert_eq!(DeviceClass::from_contexts(&contexts), None);
277    }
278}