Skip to main content

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    /// Approximate geographical location of the end user or device.
68    #[metastructure(skip_serialization = "empty")]
69    pub geo: Annotated<Geo>,
70
71    /// The user segment, for apps that divide users in user segments.
72    #[metastructure(skip_serialization = "empty")]
73    pub segment: Annotated<String>,
74
75    /// Additional arbitrary fields, as stored in the database (and sometimes as sent by clients).
76    /// All data from `self.other` should end up here after store normalization.
77    #[metastructure(pii = "true", skip_serialization = "empty")]
78    pub data: Annotated<Object<Value>>,
79
80    /// Additional arbitrary fields, as sent by clients.
81    #[metastructure(additional_properties, pii = "true")]
82    pub other: Object<Value>,
83}
84
85#[cfg(test)]
86mod tests {
87    use similar_asserts::assert_eq;
88
89    use super::*;
90    use relay_protocol::{Error, Map};
91
92    #[test]
93    fn test_geo_roundtrip() {
94        let json = r#"{
95  "country_code": "US",
96  "city": "San Francisco",
97  "subdivision": "California",
98  "region": "CA",
99  "other": "value"
100}"#;
101        let geo = Annotated::new(Geo {
102            country_code: Annotated::new("US".to_owned()),
103            city: Annotated::new("San Francisco".to_owned()),
104            subdivision: Annotated::new("California".to_owned()),
105            region: Annotated::new("CA".to_owned()),
106            other: {
107                let mut map = Map::new();
108                map.insert(
109                    "other".to_owned(),
110                    Annotated::new(Value::String("value".to_owned())),
111                );
112                map
113            },
114        });
115
116        assert_eq!(geo, Annotated::from_json(json).unwrap());
117        assert_eq!(json, geo.to_json_pretty().unwrap());
118    }
119
120    #[test]
121    fn test_geo_default_values() {
122        let json = "{}";
123        let geo = Annotated::new(Geo {
124            country_code: Annotated::empty(),
125            city: Annotated::empty(),
126            subdivision: Annotated::empty(),
127            region: Annotated::empty(),
128            other: Object::default(),
129        });
130
131        assert_eq!(geo, Annotated::from_json(json).unwrap());
132        assert_eq!(json, geo.to_json_pretty().unwrap());
133    }
134
135    #[test]
136    fn test_user_roundtrip() {
137        let json = r#"{
138  "id": "e4e24881-8238-4539-a32b-d3c3ecd40568",
139  "email": "mail@example.org",
140  "ip_address": "{{auto}}",
141  "username": "john_doe",
142  "name": "John Doe",
143  "segment": "vip",
144  "data": {
145    "data": "value"
146  },
147  "other": "value"
148}"#;
149        let user = Annotated::new(User {
150            id: Annotated::new("e4e24881-8238-4539-a32b-d3c3ecd40568".to_owned().into()),
151            email: Annotated::new("mail@example.org".to_owned()),
152            ip_address: Annotated::new(IpAddr::auto()),
153            name: Annotated::new("John Doe".to_owned()),
154            username: Annotated::new(LenientString("john_doe".to_owned())),
155            geo: Annotated::empty(),
156            segment: Annotated::new("vip".to_owned()),
157            data: {
158                let mut map = Object::new();
159                map.insert(
160                    "data".to_owned(),
161                    Annotated::new(Value::String("value".to_owned())),
162                );
163                Annotated::new(map)
164            },
165            other: {
166                let mut map = Object::new();
167                map.insert(
168                    "other".to_owned(),
169                    Annotated::new(Value::String("value".to_owned())),
170                );
171                map
172            },
173        });
174
175        assert_eq!(user, Annotated::from_json(json).unwrap());
176        assert_eq!(json, user.to_json_pretty().unwrap());
177    }
178
179    #[test]
180    fn test_user_lenient_id() {
181        let input = r#"{"id":42}"#;
182        let output = r#"{"id":"42"}"#;
183        let user = Annotated::new(User {
184            id: Annotated::new("42".to_owned().into()),
185            ..User::default()
186        });
187
188        assert_eq!(user, Annotated::from_json(input).unwrap());
189        assert_eq!(output, user.to_json().unwrap());
190    }
191
192    #[test]
193    fn test_user_lenient_username() {
194        let input = r#"{"username":42}"#;
195        let output = r#"{"username":"42"}"#;
196        let user = Annotated::new(User {
197            username: Annotated::new("42".to_owned().into()),
198            ..User::default()
199        });
200
201        assert_eq!(user, Annotated::from_json(input).unwrap());
202        assert_eq!(output, user.to_json().unwrap());
203    }
204
205    #[test]
206    fn test_user_invalid_id() {
207        let json = r#"{"id":[]}"#;
208        let user = Annotated::new(User {
209            id: Annotated::from_error(
210                Error::expected("a primitive value"),
211                Some(Value::Array(vec![])),
212            ),
213            ..User::default()
214        });
215
216        assert_eq!(user, Annotated::from_json(json).unwrap());
217    }
218
219    #[test]
220    fn test_explicit_none() {
221        let json = r#"{
222  "id": null
223}"#;
224
225        let user = Annotated::new(User::default());
226
227        assert_eq!(user, Annotated::from_json(json).unwrap());
228        assert_eq!("{}", user.to_json_pretty().unwrap());
229    }
230}