Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/app/core/reports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ pub enum Dimension {
UtmCampaign,
UtmContent,
UtmTerm,
ScreenResolution,
}

#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, Copy, Hash, Eq, PartialEq, PartialOrd, Ord)]
Expand Down Expand Up @@ -181,6 +182,9 @@ fn filter_sql(filters: &[DimensionFilter]) -> Result<(String, ParamVec<'_>)> {
Dimension::UtmCampaign => format!("utm_campaign {filter_value}"),
Dimension::UtmContent => format!("utm_content {filter_value}"),
Dimension::UtmTerm => format!("utm_term {filter_value}"),
Dimension::ScreenResolution => {
format!("concat_ws('x', screen_width::text, screen_height::text) {filter_value}")
}
})
})
.collect::<Result<Vec<String>>>()?;
Expand Down Expand Up @@ -457,6 +461,9 @@ pub fn dimension_report(
Dimension::UtmCampaign => ("utm_campaign", "utm_campaign"),
Dimension::UtmContent => ("utm_content", "utm_content"),
Dimension::UtmTerm => ("utm_term", "utm_term"),
Dimension::ScreenResolution => {
("nullif(concat_ws('x', screen_width::text, screen_height::text), '')", "screen_width, screen_height")
}
};

params.push(range.start);
Expand Down
4 changes: 4 additions & 0 deletions src/app/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ pub struct Event {
pub utm_campaign: Option<String>,
pub utm_content: Option<String>,
pub utm_term: Option<String>,
pub screen_width: Option<i32>,
pub screen_height: Option<i32>,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -100,6 +102,8 @@ macro_rules! event_params {
$event.utm_term,
None::<std::time::Duration>,
None::<std::time::Duration>,
$event.screen_width,
$event.screen_height,
]
};
}
Expand Down
2 changes: 2 additions & 0 deletions src/migrations/events/V5__screen_size.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
alter table events add column screen_width integer;
alter table events add column screen_height integer;
2 changes: 2 additions & 0 deletions src/utils/seed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ pub fn random_events(
utm_medium: Some(random_el(UTM_MEDIUMS, 0.6).to_string()),
utm_source: Some(random_el(UTM_SOURCES, 0.6).to_string()),
utm_term: Some(random_el(UTM_TERMS, 0.6).to_string()),
screen_width: None,
screen_height: None,
})
})
}
Expand Down
3 changes: 3 additions & 0 deletions src/web/routes/dashboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,9 @@ async fn project_detailed_handler(
let city = city.filter(|city| !city.is_empty());
data.push(DimensionTableRow { dimension_value: key, value, display_name: city, icon: country });
}
Dimension::ScreenResolution => {
data.push(DimensionTableRow { dimension_value: key, value, display_name: None, icon: None });
}
_ => {
data.push(DimensionTableRow { dimension_value: key, value, display_name: None, icon: None });
}
Expand Down
4 changes: 4 additions & 0 deletions src/web/routes/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ struct EventRequest {
url: String,
referrer: Option<String>,
utm: Option<Utm>,
screen_width: Option<i32>,
screen_height: Option<i32>,
}

#[derive(serde::Deserialize, JsonSchema)]
Expand Down Expand Up @@ -124,6 +126,8 @@ fn process_event(
utm_medium: event.utm.as_ref().and_then(|u| u.medium.clone()),
utm_source: event.utm.as_ref().and_then(|u| u.source.clone()),
utm_term: event.utm.as_ref().and_then(|u| u.term.clone()),
screen_width: event.screen_width,
screen_height: event.screen_height,
};

events.send(event)?;
Expand Down
3 changes: 3 additions & 0 deletions tests/dashboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ async fn test_dashboard() -> Result<()> {
json!({"dimension":"url","filters":[{"dimension":"fqdn","filterType":"equal","value":"example.org"},{"dimension":"url","filterType":"equal","value":"example.org/contact"},{"dimension":"referrer","filterType":"equal","value":"liwan.dev"},{"dimension":"country","filterType":"equal","value":"AU"},{"dimension":"city","filterType":"equal","value":"Sydney"},{"dimension":"platform","filterType":"equal","value":"iOS"},{"dimension":"browser","filterType":"equal","value":"Safari"},{"dimension":"mobile","filterType":"is_true"}],"metric":"views","range":{"start": start_date ,"end": end_date}}),
json!({"dimension":"city","filters":[{"dimension":"fqdn","filterType":"equal","value":"example.org"},{"dimension":"url","filterType":"equal","value":"example.org/contact"},{"dimension":"referrer","filterType":"equal","value":"liwan.dev"},{"dimension":"country","filterType":"equal","value":"AU"},{"dimension":"city","filterType":"equal","value":"Sydney"},{"dimension":"platform","filterType":"equal","value":"iOS"},{"dimension":"browser","filterType":"equal","value":"Safari"},{"dimension":"mobile","filterType":"is_true"}],"metric":"views","range":{"start": start_date ,"end": end_date}}),
json!({"dimension":"browser","filters":[{"dimension":"fqdn","filterType":"equal","value":"example.org"},{"dimension":"url","filterType":"equal","value":"example.org/contact"},{"dimension":"referrer","filterType":"equal","value":"liwan.dev"},{"dimension":"country","filterType":"equal","value":"AU"},{"dimension":"city","filterType":"equal","value":"Sydney"},{"dimension":"platform","filterType":"equal","value":"iOS"},{"dimension":"browser","filterType":"equal","value":"Safari"},{"dimension":"mobile","filterType":"is_true"}],"metric":"views","range":{"start": start_date ,"end": end_date}}),
json!({"dimension":"screen_resolution","filters":[],"metric":"views","range":{"start": start_date ,"end": end_date}}),
json!({"dimension":"screen_resolution","filters":[],"metric":"unique_visitors","range":{"start": start_date ,"end": end_date}}),
json!({"dimension":"url","filters":[{"dimension":"screen_resolution","filterType":"equal","value":"1920x1080"}],"metric":"views","range":{"start": start_date ,"end": end_date}}),
];

for request in stats_requests.iter() {
Expand Down
135 changes: 135 additions & 0 deletions tests/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,138 @@ async fn test_event() -> Result<()> {

Ok(())
}

#[tokio::test]
async fn test_event_screen_size_accepted() -> Result<()> {
let app = common::app();
let (tx, _rx) = common::events();
let client = common::TestClient::new(app.clone(), tx);
app.entities.create(&Entity { display_name: "Entity 1".to_string(), id: "entity-1".to_string() }, &[])?;

let user_agent = vec![("user-agent".to_string(), "Mozilla/5.0 (Linux x86_64)".to_string())];
let event_desktop_screen_size = json!({
"entity_id": "entity-1",
"name": "pageview",
"url": "https://example.com/",
"screen_width": 1920,
"screen_height": 1080
});

let res = client.post_with_headers("/api/event", event_desktop_screen_size.clone(), user_agent.clone()).await;
res.assert_status_success();

let event_mobile_screen_size = json!({
"entity_id": "entity-1",
"name": "pageview",
"url": "https://example.com/",
"screen_width": 390,
"screen_height": 844
});

let res = client.post_with_headers("/api/event", event_mobile_screen_size.clone(), user_agent.clone()).await;
res.assert_status_success();

Ok(())
}

#[tokio::test]
async fn test_event_screen_size_read_write() -> Result<()> {
use chrono::Utc;
use liwan::app::models::Event;

let app = common::app();
let (tx, _rx) = common::events();
let client = common::TestClient::new(app.clone(), tx);

app.seed_database(0)?;

let events_to_insert = vec![
Event {
entity_id: "entity-1".to_string(),
visitor_id: "visitor-desktop".to_string(),
event: "pageview".to_string(),
created_at: Utc::now(),
fqdn: Some("example.com".to_string()),
path: Some("/".to_string()),
referrer: None,
platform: None,
browser: None,
mobile: Some(false),
country: None,
city: None,
utm_source: None,
utm_medium: None,
utm_campaign: None,
utm_content: None,
utm_term: None,
screen_width: Some(1920),
screen_height: Some(1080),
},
Event {
entity_id: "entity-1".to_string(),
visitor_id: "visitor-mobile".to_string(),
event: "pageview".to_string(),
created_at: Utc::now(),
fqdn: Some("example.com".to_string()),
path: Some("/".to_string()),
referrer: None,
platform: None,
browser: None,
mobile: Some(true),
country: None,
city: None,
utm_source: None,
utm_medium: None,
utm_campaign: None,
utm_content: None,
utm_term: None,
screen_width: Some(390),
screen_height: Some(844),
},
Event {
entity_id: "entity-1".to_string(),
visitor_id: "visitor-old".to_string(),
event: "pageview".to_string(),
created_at: Utc::now(),
fqdn: Some("example.com".to_string()),
path: Some("/".to_string()),
referrer: None,
platform: None,
browser: None,
mobile: None,
country: None,
city: None,
utm_source: None,
utm_medium: None,
utm_campaign: None,
utm_content: None,
utm_term: None,
screen_width: None,
screen_height: None,
},
];
app.events.append(events_to_insert.into_iter())?;

let start = (Utc::now() - chrono::Duration::hours(1)).to_rfc3339();
let end = Utc::now().to_rfc3339();

let query = json!({
"dimension": "screen_resolution",
"filters": [],
"metric": "views",
"range": { "start": start, "end": end }
});

let res = client.post("/api/dashboard/project/public-project/dimension", query.clone()).await;
res.assert_status_success();

let body: serde_json::Value = res.json();
let rows = body["data"].as_array().expect("expected data array");
let values: Vec<&str> = rows.iter().filter_map(|r| r["dimensionValue"].as_str()).collect();

assert!(values.contains(&"1920x1080"), "expected 1920x1080 in results, got: {values:?}");
assert!(values.contains(&"390x844"), "expected 390x844 in results, got: {values:?}");
assert!(values.contains(&"Unknown"), "expected Unknown for NULL screen data, got: {values:?}");

Ok(())
}
2 changes: 1 addition & 1 deletion tracker/script.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions tracker/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ type Payload = {
name: string;
url: string;
referrer?: string;
screen_width?: number;
screen_height?: number;
utm?: { campaign?: string; content?: string; medium?: string; source?: string; term?: string };
};

Expand Down Expand Up @@ -100,6 +102,8 @@ export async function event(name: string = "pageview", options?: EventOptions):
entity_id: options?.entity || entity,
referrer: options?.referrer || referrer,
url: options?.url || location.origin + location.pathname,
screen_width: !noWindow ? window.screen?.width : undefined,
screen_height: !noWindow ? window.screen?.height : undefined,
utm: {
campaign: utm.get("utm_campaign"),
content: utm.get("utm_content"),
Expand Down
1 change: 1 addition & 0 deletions web/src/api/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const dimensionNames: Record<Dimension, string> = {
utm_medium: "UTM Medium",
utm_source: "UTM Source",
utm_term: "UTM Term",
screen_resolution: "Screen Resolution",
};

export const filterNames: Record<DimensionFilter["filterType"], string> = {
Expand Down
Loading