analytics_next/
lib.rs

1pub use analytics_next_sys as sys;
2use gloo_utils::format::JsValueSerdeExt;
3use js_sys::{Object, Reflect};
4use serde_json::Value;
5use std::borrow::Cow;
6use std::collections::HashMap;
7use std::rc::Rc;
8use wasm_bindgen::prelude::*;
9
10/// A macro to turn a [`serde::Serialize`] into a [`TrackingEvent`].
11///
12/// ## Example
13///
14/// ```rust
15/// # use analytics_next_macro::tracking;
16/// #[derive(serde::Serialize)]
17/// #[tracking("My event")]
18/// struct MyEvent {}
19/// ```
20pub use analytics_next_macro::tracking;
21
22/// The analytics client instance
23#[derive(Clone)]
24pub struct AnalyticsBrowser {
25    pub instance: Rc<sys::AnalyticsBrowser>,
26}
27
28impl PartialEq for AnalyticsBrowser {
29    fn eq(&self, other: &Self) -> bool {
30        Rc::ptr_eq(&self.instance, &other.instance)
31    }
32}
33
34/// The client settings
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct Settings {
37    pub write_key: String,
38}
39
40impl From<Settings> for JsValue {
41    fn from(value: Settings) -> Self {
42        let result = Object::new();
43        let _ = Reflect::set(&result, &"writeKey".into(), &value.write_key.into());
44        result.into()
45    }
46}
47
48/// A tracking event
49#[derive(Clone, Debug, PartialEq, Eq)]
50pub struct TrackingEvent<'a> {
51    /// The name of the event
52    pub event: Cow<'a, str>,
53    /// The event payload
54    pub payload: Option<Value>,
55    /// Additional options
56    pub options: Option<Value>,
57}
58
59impl<'a> From<&'a str> for TrackingEvent<'a> {
60    fn from(event: &'a str) -> Self {
61        TrackingEvent {
62            event: event.into(),
63            payload: None,
64            options: None,
65        }
66    }
67}
68
69impl From<String> for TrackingEvent<'static> {
70    fn from(event: String) -> Self {
71        TrackingEvent {
72            event: event.into(),
73            payload: None,
74            options: None,
75        }
76    }
77}
78
79impl From<(String, Value)> for TrackingEvent<'static> {
80    fn from((event, payload): (String, Value)) -> Self {
81        TrackingEvent {
82            event: event.into(),
83            payload: Some(payload),
84            options: None,
85        }
86    }
87}
88
89impl<'a> From<(&'a str, Value)> for TrackingEvent<'a> {
90    fn from((event, payload): (&'a str, Value)) -> Self {
91        TrackingEvent {
92            event: event.into(),
93            payload: Some(payload),
94            options: None,
95        }
96    }
97}
98
99impl<'a> From<(&'a str, Option<Value>)> for TrackingEvent<'a> {
100    fn from((event, payload): (&'a str, Option<Value>)) -> Self {
101        TrackingEvent {
102            event: event.into(),
103            payload,
104            options: None,
105        }
106    }
107}
108
109/// A user
110#[derive(Clone, Debug, Default, PartialEq, Eq)]
111pub struct User {
112    /// The ID of the user, [`None`] if it isn't known
113    pub id: Option<String>,
114    /// User traits
115    pub traits: Value,
116    /// Additional options
117    pub options: Value,
118}
119
120impl AnalyticsBrowser {
121    /// Load a default instance
122    pub fn load(settings: Settings) -> Self {
123        Self::load_with_options(settings, JsValue::UNDEFINED)
124    }
125
126    /// Load with custom options
127    pub fn load_with_options(settings: Settings, options: JsValue) -> Self {
128        let instance = sys::AnalyticsBrowser::new();
129        instance.load(settings.into(), options);
130        Self {
131            instance: Rc::new(instance),
132        }
133    }
134
135    /// Identify the current session
136    pub fn identify(&self, user: impl Into<User>) {
137        let user = user.into();
138        let id = user.id.as_deref();
139        let traits = JsValue::from_serde(&user.traits).unwrap_or(JsValue::NULL);
140        let options = JsValue::from_serde(&user.options).unwrap_or(JsValue::NULL);
141
142        self.instance.identify(id, traits, options);
143    }
144
145    /// Track an event
146    pub fn track<'a>(&self, event: impl Into<TrackingEvent<'a>>) {
147        let event = event.into();
148
149        let name = &event.event;
150        let properties = event
151            .payload
152            .and_then(|value| JsValue::from_serde(&value).ok())
153            .unwrap_or(JsValue::UNDEFINED);
154        let options = event
155            .options
156            .and_then(|value| JsValue::from_serde(&value).ok())
157            .unwrap_or(JsValue::UNDEFINED);
158
159        self.instance.track(name, properties, options);
160    }
161
162    #[must_use = "A tracking operation must be completed by calling the TrackBuilder::complete() function"]
163    pub fn build(&self, event: impl Into<String>) -> TrackBuilder<'_> {
164        let event = event.into();
165        TrackBuilder {
166            instance: &self.instance,
167            event,
168            properties: HashMap::new(),
169            options: JsValue::UNDEFINED,
170        }
171    }
172
173    /// Track the current page
174    pub fn page(&self) {
175        self.instance.page();
176    }
177}
178
179pub struct TrackBuilder<'a> {
180    pub instance: &'a sys::AnalyticsBrowser,
181    pub event: String,
182    pub properties: HashMap<String, Value>,
183    pub options: JsValue,
184}
185
186impl<'a> TrackBuilder<'a> {
187    pub fn options(mut self, options: JsValue) -> Self {
188        self.options = options;
189        self
190    }
191
192    pub fn properties(mut self, properties: HashMap<String, Value>) -> Self {
193        self.properties = properties;
194        self
195    }
196
197    pub fn extend_properties(
198        mut self,
199        properties: impl IntoIterator<Item = (String, Value)>,
200    ) -> Self {
201        self.properties.extend(properties);
202        self
203    }
204
205    pub fn add_property(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
206        self.properties.insert(key.into(), value.into());
207        self
208    }
209
210    pub fn complete(self) {
211        if let Ok(properties) = JsValue::from_serde(&self.properties) {
212            self.instance.track(&self.event, properties, self.options);
213        }
214    }
215}