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