segment/
message.rs

1//! Representations of the messages which may be sent to Segment's tracking API.
2//!
3//! All Segment messages support a few common fields:
4//!
5//! * Details related to user identification are captured by this library
6//!   through the [`User`](enum.User.html) enum.
7//!
8//! * Some user traits and event properties are specified through the Segment
9//!   spec -- these are standardized members which, if followed, will be
10//!   converted to the native equivalent of each tool.
11//!
12//!     * Standardized event names and properties are specified in [Segment's
13//!       semantic events docs](https://segment.com/docs/spec/semantic/).
14//!     * Standardized user traits are specified in [Segment's `identify` traits
15//!       docs](https://segment.com/docs/spec/identify/#traits).
16//!     * Standardized group traits are specified in [Segment's `group` traits
17//!       docs](https://segment.com/docs/spec/group/#traits).
18//!
19//! * All Segment messages support a `context` field containing additional
20//!   contextual details. This field is exposed in this library as `context`.
21//!   The data in `context` is standardized, and is documented in [Segment's
22//!   context docs](https://segment.com/docs/spec/common/#context).
23//!
24//! * All Segment messages support an `integrations` field that enables simple
25//!   routing at the event collection layer. See [Segment's `integrations`
26//!   docs](https://segment.com/docs/spec/common/#integrations) for how to use
27//!   this field.
28
29use std::fmt::Display;
30
31use serde::{Deserialize, Serialize};
32use serde_json::{Map, Value};
33use time::OffsetDateTime;
34
35/// An enum containing all values which may be sent to Segment's tracking API.
36#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
37#[serde(untagged)]
38pub enum Message {
39    Identify(Identify),
40    Track(Track),
41    Page(Page),
42    Screen(Screen),
43    Group(Group),
44    Alias(Alias),
45    Batch(Batch),
46}
47
48/// An identify event.
49///
50/// See [Segment's documentation](https://segment.com/docs/spec/identify/) for
51/// how to use `identify` events.
52#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
53pub struct Identify {
54    /// The user associated with this message.
55    #[serde(flatten)]
56    pub user: User,
57
58    /// The traits to assign to the user.
59    pub traits: Value,
60
61    /// The timestamp associated with this message.
62    #[serde(
63        skip_serializing_if = "Option::is_none",
64        with = "time::serde::rfc3339::option"
65    )]
66    pub timestamp: Option<OffsetDateTime>,
67
68    /// Context associated with this message.
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub context: Option<Value>,
71
72    /// Integrations to route this message to.
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub integrations: Option<Value>,
75
76    /// Extra fields to put at the top level of this message.
77    #[serde(flatten)]
78    pub extra: Map<String, Value>,
79}
80
81/// A track event.
82///
83/// See [Segment's documentation](https://segment.com/docs/spec/track/) for
84/// how to use `track` events.
85#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
86pub struct Track {
87    /// The user associated with this message.
88    #[serde(flatten)]
89    pub user: User,
90
91    /// The name of the event being tracked.
92    pub event: String,
93
94    /// The properties associated with the event.
95    pub properties: Value,
96
97    /// The timestamp associated with this message.
98    #[serde(
99        skip_serializing_if = "Option::is_none",
100        with = "time::serde::rfc3339::option"
101    )]
102    pub timestamp: Option<OffsetDateTime>,
103
104    /// Context associated with this message.
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub context: Option<Value>,
107
108    /// Integrations to route this message to.
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub integrations: Option<Value>,
111
112    /// Extra fields to put at the top level of this message.
113    #[serde(flatten)]
114    pub extra: Map<String, Value>,
115}
116
117/// A page event.
118///
119/// See [Segment's documentation](https://segment.com/docs/spec/page/) for how
120/// to use `page` events.
121#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
122pub struct Page {
123    /// The user associated with this message.
124    #[serde(flatten)]
125    pub user: User,
126
127    /// The name of the page being tracked.
128    pub name: String,
129
130    /// The properties associated with the event.
131    pub properties: Value,
132
133    /// The timestamp associated with this message.
134    #[serde(
135        skip_serializing_if = "Option::is_none",
136        with = "time::serde::rfc3339::option"
137    )]
138    pub timestamp: Option<OffsetDateTime>,
139
140    /// Context associated with this message.
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub context: Option<Value>,
143
144    /// Integrations to route this message to.
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub integrations: Option<Value>,
147
148    /// Extra fields to put at the top level of this message.
149    #[serde(flatten)]
150    pub extra: Map<String, Value>,
151}
152
153/// A screen event.
154///
155/// See [Segment's documentation](https://segment.com/docs/spec/screen/) for how
156/// to use `screen` events.
157#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
158pub struct Screen {
159    /// The user associated with this message.
160    #[serde(flatten)]
161    pub user: User,
162
163    /// The name of the screen being tracked.
164    pub name: String,
165
166    /// The properties associated with the event.
167    pub properties: Value,
168
169    /// The timestamp associated with this message.
170    #[serde(
171        skip_serializing_if = "Option::is_none",
172        with = "time::serde::rfc3339::option"
173    )]
174    pub timestamp: Option<OffsetDateTime>,
175
176    /// Context associated with this message.
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub context: Option<Value>,
179
180    /// Integrations to route this message to.
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub integrations: Option<Value>,
183
184    /// Extra fields to put at the top level of this message.
185    #[serde(flatten)]
186    pub extra: Map<String, Value>,
187}
188
189/// A group event.
190///
191/// See [Segment's documentation](https://segment.com/docs/spec/group/) for how
192/// to use `group` events.
193#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
194pub struct Group {
195    /// The user associated with this message.
196    #[serde(flatten)]
197    pub user: User,
198
199    /// The group the user is being associated with.
200    #[serde(rename = "groupId")]
201    pub group_id: String,
202
203    /// The traits to assign to the group.
204    pub traits: Value,
205
206    /// The timestamp associated with this message.
207    #[serde(
208        skip_serializing_if = "Option::is_none",
209        with = "time::serde::rfc3339::option"
210    )]
211    pub timestamp: Option<OffsetDateTime>,
212
213    /// Context associated with this message.
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub context: Option<Value>,
216
217    /// Integrations to route this message to.
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub integrations: Option<Value>,
220
221    /// Extra fields to put at the top level of this message.
222    #[serde(flatten)]
223    pub extra: Map<String, Value>,
224}
225
226/// An alias event.
227///
228/// See [Segment's documentation](https://segment.com/docs/spec/alias/) for how
229/// to use `alias` events.
230#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
231pub struct Alias {
232    /// The user associated with this message.
233    #[serde(flatten)]
234    pub user: User,
235
236    /// The user's previous ID.
237    #[serde(rename = "previousId")]
238    pub previous_id: String,
239
240    /// The timestamp associated with this message.
241    #[serde(
242        skip_serializing_if = "Option::is_none",
243        with = "time::serde::rfc3339::option"
244    )]
245    pub timestamp: Option<OffsetDateTime>,
246
247    /// Context associated with this message.
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub context: Option<Value>,
250
251    /// Integrations to route this message to.
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub integrations: Option<Value>,
254
255    /// Extra fields to put at the top level of this message.
256    #[serde(flatten)]
257    pub extra: Map<String, Value>,
258}
259
260/// A batch of events.
261///
262/// See [Segment's
263/// documentation](https://segment.com/docs/sources/server/http/#batch) for how
264/// to send batches of events.
265#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)]
266pub struct Batch {
267    /// The batch of messages to send.
268    pub batch: Vec<BatchMessage>,
269
270    /// Context associated with this message.
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub context: Option<Value>,
273
274    /// Integrations to route this message to.
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub integrations: Option<Value>,
277
278    /// Extra fields to put at the top level of this message.
279    #[serde(flatten)]
280    pub extra: Map<String, Value>,
281}
282
283/// An enum containing all messages which may be placed inside a batch.
284#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
285#[serde(tag = "type")]
286pub enum BatchMessage {
287    #[serde(rename = "identify")]
288    Identify(Identify),
289    #[serde(rename = "track")]
290    Track(Track),
291    #[serde(rename = "page")]
292    Page(Page),
293    #[serde(rename = "screen")]
294    Screen(Screen),
295    #[serde(rename = "group")]
296    Group(Group),
297    #[serde(rename = "alias")]
298    Alias(Alias),
299}
300
301impl BatchMessage {
302    pub(crate) fn timestamp_mut(&mut self) -> &mut Option<OffsetDateTime> {
303        match self {
304            Self::Identify(identify) => &mut identify.timestamp,
305            Self::Track(track) => &mut track.timestamp,
306            Self::Page(page) => &mut page.timestamp,
307            Self::Screen(screen) => &mut screen.timestamp,
308            Self::Group(group) => &mut group.timestamp,
309            Self::Alias(alias) => &mut alias.timestamp,
310        }
311    }
312}
313
314/// User ID information.
315///
316/// All Segment tracking API calls require a user ID, an anonymous ID, or both.
317/// See [Segment's
318/// documentation](https://segment.com/docs/spec/identify/#identities) for how
319/// user IDs and anonymous IDs should be used.
320#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
321#[serde(untagged)]
322pub enum User {
323    /// The user is identified only by a user ID.
324    UserId {
325        #[serde(rename = "userId")]
326        user_id: String,
327    },
328
329    /// The user is identified only by an anonymous ID.
330    AnonymousId {
331        #[serde(rename = "anonymousId")]
332        anonymous_id: String,
333    },
334
335    /// The user is identified by both a user ID and an anonymous ID.
336    Both {
337        #[serde(rename = "userId")]
338        user_id: String,
339
340        #[serde(rename = "anonymousId")]
341        anonymous_id: String,
342    },
343}
344
345impl Display for User {
346    /// Display a `UserId`. If he has both an `anonymous_id` and a `user_id` we display the `user_id`
347    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
348        match self {
349            User::UserId { user_id } => write!(f, "{}", user_id),
350            User::AnonymousId { anonymous_id } => write!(f, "{}", anonymous_id),
351            User::Both { user_id, .. } => write!(f, "{}", user_id),
352        }
353    }
354}
355
356impl Default for User {
357    fn default() -> Self {
358        User::AnonymousId {
359            anonymous_id: "".to_owned(),
360        }
361    }
362}
363
364macro_rules! into {
365    (from $from:ident into $for:ident) => {
366        impl From<$from> for $for {
367            fn from(message: $from) -> Self {
368                Self::$from(message)
369            }
370        }
371    };
372    ($(from $from:ident into $for:ident),+ $(,)?) => {
373        $(
374            into!{from $from into $for}
375        )+
376    };
377}
378
379into! {
380    from Identify into Message,
381    from Track into Message,
382    from Page into Message,
383    from Screen into Message,
384    from Group into Message,
385    from Alias into Message,
386    from Batch into Message,
387
388    from Identify into BatchMessage,
389    from Track into BatchMessage,
390    from Page into BatchMessage,
391    from Screen into BatchMessage,
392    from Group into BatchMessage,
393    from Alias into BatchMessage,
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399    use serde_json::json;
400
401    #[test]
402    fn serialize() {
403        assert_eq!(
404            serde_json::to_string(&Message::Identify(Identify {
405                user: User::UserId {
406                    user_id: "foo".to_owned()
407                },
408                traits: json!({
409                    "foo": "bar",
410                    "baz": "quux",
411                }),
412                extra: [("messageId".to_owned(), json!("123"))]
413                    .iter()
414                    .cloned()
415                    .collect(),
416                ..Default::default()
417            }))
418            .unwrap(),
419            r#"{"userId":"foo","traits":{"baz":"quux","foo":"bar"},"messageId":"123"}"#.to_owned(),
420        );
421
422        assert_eq!(
423            serde_json::to_string(&Message::Track(Track {
424                user: User::AnonymousId {
425                    anonymous_id: "foo".to_owned()
426                },
427                event: "Foo".to_owned(),
428                properties: json!({
429                    "foo": "bar",
430                    "baz": "quux",
431                }),
432                ..Default::default()
433            }))
434            .unwrap(),
435            r#"{"anonymousId":"foo","event":"Foo","properties":{"baz":"quux","foo":"bar"}}"#
436                .to_owned(),
437        );
438
439        assert_eq!(
440            serde_json::to_string(&Message::Page(Page {
441                user: User::Both {
442                    user_id: "foo".to_owned(),
443                    anonymous_id: "bar".to_owned()
444                },
445                name: "Foo".to_owned(),
446                properties: json!({
447                    "foo": "bar",
448                    "baz": "quux",
449                }),
450                ..Default::default()
451            }))
452            .unwrap(),
453            r#"{"userId":"foo","anonymousId":"bar","name":"Foo","properties":{"baz":"quux","foo":"bar"}}"#
454                .to_owned(),
455        );
456
457        assert_eq!(
458            serde_json::to_string(&Message::Screen(Screen {
459                user: User::Both {
460                    user_id: "foo".to_owned(),
461                    anonymous_id: "bar".to_owned()
462                },
463                name: "Foo".to_owned(),
464                properties: json!({
465                    "foo": "bar",
466                    "baz": "quux",
467                }),
468                ..Default::default()
469            }))
470            .unwrap(),
471            r#"{"userId":"foo","anonymousId":"bar","name":"Foo","properties":{"baz":"quux","foo":"bar"}}"#
472                .to_owned(),
473        );
474
475        assert_eq!(
476            serde_json::to_string(&Message::Group(Group {
477                user: User::UserId {
478                    user_id: "foo".to_owned()
479                },
480                group_id: "bar".to_owned(),
481                traits: json!({
482                    "foo": "bar",
483                    "baz": "quux",
484                }),
485                ..Default::default()
486            }))
487            .unwrap(),
488            r#"{"userId":"foo","groupId":"bar","traits":{"baz":"quux","foo":"bar"}}"#.to_owned(),
489        );
490
491        assert_eq!(
492            serde_json::to_string(&Message::Alias(Alias {
493                user: User::UserId {
494                    user_id: "foo".to_owned()
495                },
496                previous_id: "bar".to_owned(),
497                ..Default::default()
498            }))
499            .unwrap(),
500            r#"{"userId":"foo","previousId":"bar"}"#.to_owned(),
501        );
502
503        assert_eq!(
504            serde_json::to_string(&Message::Batch(Batch {
505                batch: vec![
506                    BatchMessage::Track(Track {
507                        user: User::UserId {
508                            user_id: "foo".to_owned()
509                        },
510                        event: "Foo".to_owned(),
511                        properties: json!({}),
512                        ..Default::default()
513                    }),
514                    BatchMessage::Track(Track {
515                        user: User::UserId {
516                            user_id: "bar".to_owned()
517                        },
518                        event: "Bar".to_owned(),
519                        properties: json!({}),
520                        ..Default::default()
521                    }),
522                    BatchMessage::Track(Track {
523                        user: User::UserId {
524                            user_id: "baz".to_owned()
525                        },
526                        event: "Baz".to_owned(),
527                        properties: json!({}),
528                        ..Default::default()
529                    })
530                ],
531                context: Some(json!({
532                    "foo": "bar",
533                })),
534                ..Default::default()
535            }))
536            .unwrap(),
537            r#"{"batch":[{"type":"track","userId":"foo","event":"Foo","properties":{}},{"type":"track","userId":"bar","event":"Bar","properties":{}},{"type":"track","userId":"baz","event":"Baz","properties":{}}],"context":{"foo":"bar"}}"#
538                .to_owned(),
539        );
540    }
541}