Skip to main content

worker/
analytics_engine.rs

1use crate::EnvBinding;
2use crate::Result;
3use js_sys::Object;
4use js_sys::{Array, Uint8Array};
5use wasm_bindgen::{JsCast, JsValue};
6use worker_sys::AnalyticsEngineDataset as AnalyticsEngineSys;
7
8#[derive(Debug, Clone)]
9pub struct AnalyticsEngineDataset(AnalyticsEngineSys);
10
11unsafe impl Send for AnalyticsEngineDataset {}
12unsafe impl Sync for AnalyticsEngineDataset {}
13
14impl EnvBinding for AnalyticsEngineDataset {
15    const TYPE_NAME: &'static str = "AnalyticsEngineDataset";
16
17    // Override get to perform an unchecked cast from an object.
18    // Miniflare defines the binding as an Object and not a class so its name is not available for checking.
19    // https://github.com/cloudflare/workers-sdk/blob/main/packages/wrangler/templates/middleware/middleware-mock-analytics-engine.ts#L6
20    fn get(val: JsValue) -> Result<Self> {
21        let obj = Object::from(val);
22        Ok(obj.unchecked_into())
23    }
24}
25
26impl JsCast for AnalyticsEngineDataset {
27    fn instanceof(val: &JsValue) -> bool {
28        val.is_instance_of::<AnalyticsEngineDataset>()
29    }
30
31    fn unchecked_from_js(val: JsValue) -> Self {
32        Self(val.into())
33    }
34
35    fn unchecked_from_js_ref(val: &JsValue) -> &Self {
36        unsafe { &*(val as *const JsValue as *const Self) }
37    }
38}
39
40impl From<AnalyticsEngineDataset> for JsValue {
41    fn from(analytics_engine: AnalyticsEngineDataset) -> Self {
42        JsValue::from(analytics_engine.0)
43    }
44}
45
46impl AsRef<JsValue> for AnalyticsEngineDataset {
47    fn as_ref(&self) -> &JsValue {
48        &self.0
49    }
50}
51
52impl AnalyticsEngineDataset {
53    pub fn write_data_point(&self, event: &AnalyticsEngineDataPoint) -> Result<()>
54    where
55        AnalyticsEngineDataPoint: Clone,
56        JsValue: From<AnalyticsEngineDataPoint>,
57    {
58        Ok(self.0.write_data_point(event.to_js_object()?)?)
59    }
60}
61
62#[derive(Debug)]
63pub enum BlobType {
64    String(String),
65    Blob(Vec<u8>),
66}
67
68impl From<BlobType> for JsValue {
69    fn from(val: BlobType) -> Self {
70        match val {
71            BlobType::String(s) => JsValue::from_str(&s),
72            BlobType::Blob(b) => {
73                let value = Uint8Array::from(b.as_slice());
74                value.into()
75            }
76        }
77    }
78}
79
80impl From<&str> for BlobType {
81    fn from(value: &str) -> Self {
82        BlobType::String(value.to_string())
83    }
84}
85
86impl From<String> for BlobType {
87    fn from(value: String) -> Self {
88        BlobType::String(value)
89    }
90}
91
92impl From<&[u8]> for BlobType {
93    fn from(value: &[u8]) -> Self {
94        BlobType::Blob(value.to_vec())
95    }
96}
97
98impl From<Vec<u8>> for BlobType {
99    fn from(value: Vec<u8>) -> Self {
100        BlobType::Blob(value)
101    }
102}
103
104impl<const COUNT: usize> From<&[u8; COUNT]> for BlobType {
105    fn from(value: &[u8; COUNT]) -> Self {
106        BlobType::Blob(value.to_vec())
107    }
108}
109
110#[derive(Debug, Clone)]
111pub struct AnalyticsEngineDataPoint {
112    indexes: Array,
113    doubles: Array,
114    blobs: Array,
115}
116
117#[derive(Debug)]
118pub struct AnalyticsEngineDataPointBuilder {
119    indexes: Array,
120    doubles: Array,
121    blobs: Array,
122}
123
124impl AnalyticsEngineDataPointBuilder {
125    pub fn new() -> Self {
126        Self {
127            indexes: Array::new(),
128            doubles: Array::new(),
129            blobs: Array::new(),
130        }
131    }
132
133    /// Sets the index values for the data point.
134    /// While the indexes field accepts an array, you currently must *only* provide a single index.
135    /// If you attempt to provide multiple indexes, your data point will not be recorded.
136    ///
137    /// # Arguments
138    ///
139    /// * `index`: A string or byte-array value to use as the index.
140    ///
141    /// returns: AnalyticsEngineDataPointBuilder
142    ///
143    /// # Examples
144    ///
145    /// ```
146    ///  use worker::AnalyticsEngineDataPointBuilder;
147    ///
148    ///  let data = AnalyticsEngineDataPointBuilder::new()
149    ///     .indexes(["index1"])
150    ///     .build();
151    /// ```
152    pub fn indexes<'index>(mut self, indexes: impl AsRef<[&'index str]>) -> Self {
153        let values = Array::new();
154        for idx in indexes.as_ref() {
155            values.push(&JsValue::from_str(idx));
156        }
157        self.indexes = values;
158        self
159    }
160
161    /// Adds a numeric value to the end of the array of doubles.
162    ///
163    /// # Arguments
164    ///
165    /// * `double`: The numeric values that you want to record in your data point
166    ///
167    /// returns: AnalyticsEngineDataPointBuilder
168    ///
169    /// # Examples
170    ///
171    /// ```
172    ///  use worker::AnalyticsEngineDataPointBuilder;
173    ///  let point = AnalyticsEngineDataPointBuilder::new()
174    ///     .indexes(["index1"])
175    ///     .add_double(25)     // double1
176    ///     .add_double(0.5)    // double2
177    ///     .build();
178    ///  println!("{:?}", point);
179    /// ```
180    pub fn add_double(self, double: impl Into<f64>) -> Self {
181        self.doubles.push(&JsValue::from_f64(double.into()));
182        self
183    }
184
185    /// Set doubles1-20 with the provide values. This method will remove any doubles previously
186    /// added using the `add_double` method.
187    ///
188    /// # Arguments
189    ///
190    /// * `doubles`: An array of doubles
191    ///
192    /// returns: AnalyticsEngineDataPointBuilder
193    ///
194    /// # Examples
195    ///
196    /// ```
197    ///  use worker::AnalyticsEngineDataPointBuilder;
198    ///  let point = AnalyticsEngineDataPointBuilder::new()
199    ///     .indexes(["index1"])
200    ///     .add_double(1) // value will be replaced by the following line
201    ///     .doubles([1, 2, 3]) // sets double1, double2 and double3
202    ///     .build();
203    ///  println!("{:?}", point);
204    /// ```
205    pub fn doubles(mut self, doubles: impl IntoIterator<Item = f64>) -> Self {
206        let values = Array::new();
207        for n in doubles {
208            values.push(&JsValue::from_f64(n));
209        }
210        self.doubles = values;
211        self
212    }
213
214    /// Adds a blob-like value to the end of the array of blobs.
215    ///
216    /// # Arguments
217    ///
218    /// * `blob`: The blob values that you want to record in your data point
219    ///
220    /// returns: AnalyticsEngineDataPointBuilder
221    ///
222    /// # Examples
223    ///
224    /// ```
225    ///  use worker::AnalyticsEngineDataPointBuilder;
226    ///  let point = AnalyticsEngineDataPointBuilder::new()
227    ///     .indexes(["index1"])
228    ///     .add_blob("Seattle")            // blob1
229    ///     .add_blob("USA")                // blob2
230    ///     .add_blob("pro_sensor_9000")    // blob3
231    ///     .build();
232    ///  println!("{:?}", point);
233    /// ```
234    pub fn add_blob(self, blob: impl Into<BlobType>) -> Self {
235        let v = blob.into();
236        self.blobs.push(&v.into());
237        self
238    }
239
240    /// Sets blobs1-20 with the provided array, replacing any values previously stored using `add_blob`.
241    ///
242    /// # Arguments
243    ///
244    /// * `blob`: The blob values that you want to record in your data point
245    ///
246    /// returns: AnalyticsEngineDataPointBuilder
247    ///
248    /// # Examples
249    ///
250    /// ```
251    ///  use worker::AnalyticsEngineDataPointBuilder;
252    ///  let point = AnalyticsEngineDataPointBuilder::new()
253    ///     .indexes(["index1"])
254    ///     .blobs(["Seattle", "USA", "pro_sensor_9000"]) // sets blob1, blob2, and blob3
255    ///     .build();
256    ///  println!("{:?}", point);
257    /// ```
258    pub fn blobs(mut self, blobs: impl IntoIterator<Item = impl Into<BlobType>>) -> Self {
259        let values = Array::new();
260        for blob in blobs {
261            let value = blob.into();
262            values.push(&value.into());
263        }
264        self.blobs = values;
265        self
266    }
267
268    pub fn build(self) -> AnalyticsEngineDataPoint {
269        AnalyticsEngineDataPoint {
270            indexes: self.indexes,
271            doubles: self.doubles,
272            blobs: self.blobs,
273        }
274    }
275
276    /// Write the data point to the provided analytics engine dataset. This is a convenience method
277    /// that can be used in place of a `.build()` followed by a call to `dataset.write_data_point(point)`.
278    ///
279    /// # Arguments
280    ///
281    /// * `dataset`: Analytics engine dataset binding
282    ///
283    /// returns: worker::Result<()>
284    ///
285    /// # Examples
286    ///
287    /// ```
288    ///  use worker::{Env, AnalyticsEngineDataPointBuilder, Response};
289    ///  use std::io::Error;
290    ///
291    ///  fn main(env: Env) -> worker::Result<Response> {
292    ///     let dataset = match env.analytics_engine("HTTP_ANALYTICS") {
293    ///         Ok(dataset) => dataset,
294    ///         Err(err) => return Response::error(format!("Failed to get dataset: {err:?}"), 500),
295    ///     };
296    ///
297    ///     AnalyticsEngineDataPointBuilder::new()
298    ///         .indexes(vec!["index1"].as_slice())
299    ///         .add_blob("GET") // blob1
300    ///         .add_double(200) // double1
301    ///         .write_to(&dataset)?;
302    ///
303    ///     Response::ok("OK")
304    /// }
305    /// ```
306    pub fn write_to(self, dataset: &AnalyticsEngineDataset) -> Result<()> {
307        dataset.write_data_point(&self.build())
308    }
309}
310
311// Implement From for JsValue separately for each type
312impl From<AnalyticsEngineDataPoint> for JsValue {
313    fn from(event: AnalyticsEngineDataPoint) -> Self {
314        let obj = Object::new();
315
316        js_sys::Reflect::set(&obj, &JsValue::from_str("indexes"), &event.indexes).unwrap();
317        js_sys::Reflect::set(&obj, &JsValue::from_str("doubles"), &event.doubles).unwrap();
318
319        let blobs = Array::new();
320        for blob in event.blobs {
321            blobs.push(&JsValue::from(&blob));
322        }
323        js_sys::Reflect::set(&obj, &JsValue::from_str("blobs"), &blobs).unwrap();
324
325        JsValue::from(obj)
326    }
327}
328
329impl AnalyticsEngineDataPoint {
330    pub fn to_js_object(&self) -> Result<JsValue>
331    where
332        Self: Clone,
333        JsValue: From<Self>,
334    {
335        Ok(self.clone().into())
336    }
337}