Skip to content

Commit c177437

Browse files
authored
Merge pull request #449 from AmbireTech/analytics-v5
Analytics recorder v5
2 parents a620dc0 + d1e83c2 commit c177437

File tree

10 files changed

+903
-13
lines changed

10 files changed

+903
-13
lines changed

primitives/src/analytics.rs

Lines changed: 222 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
use crate::ChannelId;
2-
use crate::DomainError;
1+
use crate::{ChannelId, DomainError};
2+
use parse_display::Display;
33
use serde::{Deserialize, Serialize};
44

55
pub const ANALYTICS_QUERY_LIMIT: u32 = 200;
@@ -21,8 +21,13 @@ pub struct AnalyticsResponse {
2121

2222
#[cfg(feature = "postgres")]
2323
pub mod postgres {
24-
use super::AnalyticsData;
25-
use tokio_postgres::Row;
24+
use super::{AnalyticsData, OperatingSystem};
25+
use bytes::BytesMut;
26+
use std::error::Error;
27+
use tokio_postgres::{
28+
types::{accepts, to_sql_checked, FromSql, IsNull, ToSql, Type},
29+
Row,
30+
};
2631

2732
impl From<&Row> for AnalyticsData {
2833
fn from(row: &Row) -> Self {
@@ -33,6 +38,34 @@ pub mod postgres {
3338
}
3439
}
3540
}
41+
42+
impl<'a> FromSql<'a> for OperatingSystem {
43+
fn from_sql(ty: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn Error + Sync + Send>> {
44+
let str_slice = <&str as FromSql>::from_sql(ty, raw)?;
45+
let os = match str_slice {
46+
"Other" => OperatingSystem::Other,
47+
"Linux" => OperatingSystem::Linux,
48+
_ => OperatingSystem::Whitelisted(str_slice.to_string()),
49+
};
50+
51+
Ok(os)
52+
}
53+
54+
accepts!(TEXT, VARCHAR);
55+
}
56+
57+
impl ToSql for OperatingSystem {
58+
fn to_sql(
59+
&self,
60+
ty: &Type,
61+
w: &mut BytesMut,
62+
) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
63+
self.to_string().to_sql(ty, w)
64+
}
65+
66+
accepts!(TEXT, VARCHAR);
67+
to_sql_checked!();
68+
}
3669
}
3770

3871
#[derive(Debug, Deserialize)]
@@ -49,6 +82,95 @@ pub struct AnalyticsQuery {
4982
pub segment_by_channel: Option<String>,
5083
}
5184

85+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Display, Hash, Eq)]
86+
#[serde(untagged, into = "String", from = "String")]
87+
pub enum OperatingSystem {
88+
Linux,
89+
#[display("{0}")]
90+
Whitelisted(String),
91+
Other,
92+
}
93+
94+
impl Default for OperatingSystem {
95+
fn default() -> Self {
96+
Self::Other
97+
}
98+
}
99+
100+
impl From<String> for OperatingSystem {
101+
fn from(operating_system: String) -> Self {
102+
match operating_system.as_str() {
103+
"Linux" => OperatingSystem::Linux,
104+
"Other" => OperatingSystem::Other,
105+
_ => OperatingSystem::Whitelisted(operating_system),
106+
}
107+
}
108+
}
109+
110+
impl From<OperatingSystem> for String {
111+
fn from(os: OperatingSystem) -> String {
112+
os.to_string()
113+
}
114+
}
115+
116+
impl OperatingSystem {
117+
pub const LINUX_DISTROS: [&'static str; 17] = [
118+
"Arch",
119+
"CentOS",
120+
"Slackware",
121+
"Fedora",
122+
"Debian",
123+
"Deepin",
124+
"elementary OS",
125+
"Gentoo",
126+
"Mandriva",
127+
"Manjaro",
128+
"Mint",
129+
"PCLinuxOS",
130+
"Raspbian",
131+
"Sabayon",
132+
"SUSE",
133+
"Ubuntu",
134+
"RedHat",
135+
];
136+
pub const WHITELISTED: [&'static str; 18] = [
137+
"Android",
138+
"Android-x86",
139+
"iOS",
140+
"BlackBerry",
141+
"Chromium OS",
142+
"Fuchsia",
143+
"Mac OS",
144+
"Windows",
145+
"Windows Phone",
146+
"Windows Mobile",
147+
"Linux",
148+
"NetBSD",
149+
"Nintendo",
150+
"OpenBSD",
151+
"PlayStation",
152+
"Tizen",
153+
"Symbian",
154+
"KAIOS",
155+
];
156+
157+
pub fn map_os(os_name: &str) -> OperatingSystem {
158+
if OperatingSystem::LINUX_DISTROS
159+
.iter()
160+
.any(|distro| os_name.eq(*distro))
161+
{
162+
OperatingSystem::Linux
163+
} else if OperatingSystem::WHITELISTED
164+
.iter()
165+
.any(|whitelisted| os_name.eq(*whitelisted))
166+
{
167+
OperatingSystem::Whitelisted(os_name.into())
168+
} else {
169+
OperatingSystem::Other
170+
}
171+
}
172+
}
173+
52174
impl AnalyticsQuery {
53175
pub fn is_valid(&self) -> Result<(), DomainError> {
54176
let valid_event_types = ["IMPRESSION", "CLICK"];
@@ -96,3 +218,99 @@ fn default_metric() -> String {
96218
fn default_timeframe() -> String {
97219
"hour".into()
98220
}
221+
222+
#[cfg(test)]
223+
mod test {
224+
use super::*;
225+
use crate::postgres::POSTGRES_POOL;
226+
use once_cell::sync::Lazy;
227+
use serde_json::{from_value, to_value, Value};
228+
use std::collections::HashMap;
229+
230+
static TEST_CASES: Lazy<HashMap<String, (OperatingSystem, Value)>> = Lazy::new(|| {
231+
vec![
232+
// Whitelisted - Android
233+
(
234+
OperatingSystem::WHITELISTED[0].to_string(),
235+
(
236+
OperatingSystem::Whitelisted("Android".into()),
237+
Value::String("Android".into()),
238+
),
239+
),
240+
// Linux - Arch
241+
(
242+
OperatingSystem::LINUX_DISTROS[0].to_string(),
243+
(OperatingSystem::Linux, Value::String("Linux".into())),
244+
),
245+
// Other - OS xxxxx
246+
(
247+
"OS xxxxx".into(),
248+
(OperatingSystem::Other, Value::String("Other".into())),
249+
),
250+
]
251+
.into_iter()
252+
.collect()
253+
});
254+
255+
#[cfg(feature = "postgres")]
256+
#[tokio::test]
257+
async fn os_to_from_sql() {
258+
let client = POSTGRES_POOL.get().await.unwrap();
259+
let sql_type = "VARCHAR";
260+
261+
for (input, _) in TEST_CASES.iter() {
262+
let actual_os = OperatingSystem::map_os(input);
263+
264+
// from SQL
265+
{
266+
let row_os: OperatingSystem = client
267+
.query_one(&*format!("SELECT '{}'::{}", actual_os, sql_type), &[])
268+
.await
269+
.unwrap()
270+
.get(0);
271+
272+
assert_eq!(
273+
&actual_os, &row_os,
274+
"expected and actual FromSql differ for {}",
275+
input
276+
);
277+
}
278+
279+
// to SQL
280+
{
281+
let row_os: OperatingSystem = client
282+
.query_one(&*format!("SELECT $1::{}", sql_type), &[&actual_os])
283+
.await
284+
.unwrap()
285+
.get(0);
286+
assert_eq!(
287+
&actual_os, &row_os,
288+
"expected and actual ToSql differ for {}",
289+
input
290+
);
291+
}
292+
}
293+
}
294+
295+
#[test]
296+
fn test_operating_system() {
297+
for (input, (expect_os, expect_json)) in TEST_CASES.iter() {
298+
let actual_os = OperatingSystem::map_os(input);
299+
300+
assert_eq!(
301+
expect_os, &actual_os,
302+
"expected and actual differ for {}",
303+
input
304+
);
305+
306+
let actual_json = to_value(&actual_os).expect("Should serialize it");
307+
308+
assert_eq!(expect_json, &actual_json);
309+
310+
let from_json: OperatingSystem =
311+
from_value(actual_json).expect("Should deserialize it");
312+
313+
assert_eq!(expect_os, &from_json, "error processing {}", input);
314+
}
315+
}
316+
}

primitives/src/campaign.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ mod campaign_id {
2929
use thiserror::Error;
3030
use uuid::Uuid;
3131

32-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3333
/// an Id of 16 bytes, (de)serialized as a `0x` prefixed hex
3434
/// In this implementation of the `CampaignId` the value is generated from a `Uuid::new_v4().to_simple()`
3535
pub struct CampaignId([u8; 16]);

primitives/src/ipfs.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,37 @@ impl Url {
9191
}
9292
}
9393

94+
#[cfg(feature = "postgres")]
95+
mod postgres {
96+
use super::IPFS;
97+
use bytes::BytesMut;
98+
use std::error::Error;
99+
use tokio_postgres::types::{accepts, to_sql_checked, FromSql, IsNull, ToSql, Type};
100+
101+
impl ToSql for IPFS {
102+
fn to_sql(
103+
&self,
104+
ty: &Type,
105+
w: &mut BytesMut,
106+
) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
107+
self.0.to_string().to_sql(ty, w)
108+
}
109+
110+
accepts!(TEXT, VARCHAR);
111+
to_sql_checked!();
112+
}
113+
114+
impl<'a> FromSql<'a> for IPFS {
115+
fn from_sql(ty: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn Error + Sync + Send>> {
116+
let str_slice = <&str as FromSql>::from_sql(ty, raw)?;
117+
118+
Ok(str_slice.parse()?)
119+
}
120+
121+
accepts!(TEXT, VARCHAR);
122+
}
123+
}
124+
94125
#[derive(Debug, Error)]
95126
pub enum UrlError {
96127
#[error("Parsing the IPFS Cid failed")]

0 commit comments

Comments
 (0)