relay_event_schema/protocol/
user.rs

1use relay_protocol::{Annotated, Empty, FromValue, IntoValue, Object, Value};
2
3use crate::processor::ProcessValue;
4use crate::protocol::{IpAddr, LenientString};
5
6/// Geographical location of the end user or device.
7#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
8#[metastructure(process_func = "process_geo")]
9pub struct Geo {
10    /// Two-letter country code (ISO 3166-1 alpha-2).
11    #[metastructure(pii = "true", max_chars = 102, max_chars_allowance = 1004)]
12    pub country_code: Annotated<String>,
13
14    /// Human readable city name.
15    #[metastructure(pii = "true", max_chars = 1024, max_chars_allowance = 100)]
16    pub city: Annotated<String>,
17
18    /// Human readable subdivision name.
19    #[metastructure(pii = "true", max_chars = 1024, max_chars_allowance = 100)]
20    pub subdivision: Annotated<String>,
21
22    /// Human readable region name or code.
23    #[metastructure(pii = "true", max_chars = 1024, max_chars_allowance = 100)]
24    pub region: Annotated<String>,
25
26    /// Additional arbitrary fields for forwards compatibility.
27    #[metastructure(additional_properties)]
28    pub other: Object<Value>,
29}
30
31/// Information about the user who triggered an event.
32///
33/// ```json
34/// {
35///   "user": {
36///     "id": "unique_id",
37///     "username": "my_user",
38///     "email": "foo@example.com",
39///     "ip_address": "127.0.0.1",
40///     "subscription": "basic"
41///   }
42/// }
43/// ```
44#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
45#[metastructure(process_func = "process_user", value_type = "User")]
46pub struct User {
47    /// Unique identifier of the user.
48    #[metastructure(pii = "true", max_chars = 128, skip_serialization = "empty")]
49    pub id: Annotated<LenientString>,
50
51    /// Email address of the user.
52    #[metastructure(pii = "true", max_chars = 75, skip_serialization = "empty")]
53    pub email: Annotated<String>,
54
55    /// Remote IP address of the user. Defaults to "{{auto}}".
56    #[metastructure(pii = "true", skip_serialization = "empty")]
57    pub ip_address: Annotated<IpAddr>,
58
59    /// Username of the user.
60    #[metastructure(pii = "true", max_chars = 128, skip_serialization = "empty")]
61    pub username: Annotated<LenientString>,
62
63    /// Human readable name of the user.
64    #[metastructure(pii = "true", max_chars = 128, skip_serialization = "empty")]
65    pub name: Annotated<String>,
66
67    /// The user string representation as handled in Sentry.
68    ///
69    /// This field is computed by concatenating the name of specific fields of the `User`
70    /// struct with their value. For example, if `id` is set, `sentry_user` will be equal to
71    /// `"id:id-of-the-user".
72    #[metastructure(pii = "true", skip_serialization = "empty")]
73    pub sentry_user: Annotated<String>,
74
75    /// Approximate geographical location of the end user or device.
76    #[metastructure(skip_serialization = "empty")]
77    pub geo: Annotated<Geo>,
78
79    /// The user segment, for apps that divide users in user segments.
80    #[metastructure(skip_serialization = "empty")]
81    pub segment: Annotated<String>,
82
83    /// Additional arbitrary fields, as stored in the database (and sometimes as sent by clients).
84    /// All data from `self.other` should end up here after store normalization.
85    #[metastructure(pii = "true", skip_serialization = "empty")]
86    pub data: Annotated<Object<Value>>,
87
88    /// Additional arbitrary fields, as sent by clients.
89    #[metastructure(additional_properties, pii = "true")]
90    pub other: Object<Value>,
91}
92
93#[cfg(test)]
94mod tests {
95    use similar_asserts::assert_eq;
96
97    use super::*;
98    use relay_protocol::{Error, Map};
99
100    #[test]
101    fn test_geo_roundtrip() {
102        let json = r#"{
103  "country_code": "US",
104  "city": "San Francisco",
105  "subdivision": "California",
106  "region": "CA",
107  "other": "value"
108}"#;
109        let geo = Annotated::new(Geo {
110            country_code: Annotated::new("US".to_string()),
111            city: Annotated::new("San Francisco".to_string()),
112            subdivision: Annotated::new("California".to_string()),
113            region: Annotated::new("CA".to_string()),
114            other: {
115                let mut map = Map::new();
116                map.insert(
117                    "other".to_string(),
118                    Annotated::new(Value::String("value".to_string())),
119                );
120                map
121            },
122        });
123
124        assert_eq!(geo, Annotated::from_json(json).unwrap());
125        assert_eq!(json, geo.to_json_pretty().unwrap());
126    }
127
128    #[test]
129    fn test_geo_default_values() {
130        let json = "{}";
131        let geo = Annotated::new(Geo {
132            country_code: Annotated::empty(),
133            city: Annotated::empty(),
134            subdivision: Annotated::empty(),
135            region: Annotated::empty(),
136            other: Object::default(),
137        });
138
139        assert_eq!(geo, Annotated::from_json(json).unwrap());
140        assert_eq!(json, geo.to_json_pretty().unwrap());
141    }
142
143    #[test]
144    fn test_user_roundtrip() {
145        let json = r#"{
146  "id": "e4e24881-8238-4539-a32b-d3c3ecd40568",
147  "email": "mail@example.org",
148  "ip_address": "{{auto}}",
149  "username": "john_doe",
150  "name": "John Doe",
151  "segment": "vip",
152  "data": {
153    "data": "value"
154  },
155  "other": "value"
156}"#;
157        let user = Annotated::new(User {
158            id: Annotated::new("e4e24881-8238-4539-a32b-d3c3ecd40568".to_string().into()),
159            email: Annotated::new("mail@example.org".to_string()),
160            ip_address: Annotated::new(IpAddr::auto()),
161            name: Annotated::new("John Doe".to_string()),
162            username: Annotated::new(LenientString("john_doe".to_owned())),
163            geo: Annotated::empty(),
164            segment: Annotated::new("vip".to_string()),
165            data: {
166                let mut map = Object::new();
167                map.insert(
168                    "data".to_string(),
169                    Annotated::new(Value::String("value".to_string())),
170                );
171                Annotated::new(map)
172            },
173            other: {
174                let mut map = Object::new();
175                map.insert(
176                    "other".to_string(),
177                    Annotated::new(Value::String("value".to_string())),
178                );
179                map
180            },
181            ..Default::default()
182        });
183
184        assert_eq!(user, Annotated::from_json(json).unwrap());
185        assert_eq!(json, user.to_json_pretty().unwrap());
186    }
187
188    #[test]
189    fn test_user_lenient_id() {
190        let input = r#"{"id":42}"#;
191        let output = r#"{"id":"42"}"#;
192        let user = Annotated::new(User {
193            id: Annotated::new("42".to_string().into()),
194            ..User::default()
195        });
196
197        assert_eq!(user, Annotated::from_json(input).unwrap());
198        assert_eq!(output, user.to_json().unwrap());
199    }
200
201    #[test]
202    fn test_user_lenient_username() {
203        let input = r#"{"username":42}"#;
204        let output = r#"{"username":"42"}"#;
205        let user = Annotated::new(User {
206            username: Annotated::new("42".to_string().into()),
207            ..User::default()
208        });
209
210        assert_eq!(user, Annotated::from_json(input).unwrap());
211        assert_eq!(output, user.to_json().unwrap());
212    }
213
214    #[test]
215    fn test_user_invalid_id() {
216        let json = r#"{"id":[]}"#;
217        let user = Annotated::new(User {
218            id: Annotated::from_error(
219                Error::expected("a primitive value"),
220                Some(Value::Array(vec![])),
221            ),
222            ..User::default()
223        });
224
225        assert_eq!(user, Annotated::from_json(json).unwrap());
226    }
227
228    #[test]
229    fn test_explicit_none() {
230        let json = r#"{
231  "id": null
232}"#;
233
234        let user = Annotated::new(User::default());
235
236        assert_eq!(user, Annotated::from_json(json).unwrap());
237        assert_eq!("{}", user.to_json_pretty().unwrap());
238    }
239}