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}