Skip to content

Commit 0459cc8

Browse files
chore: add more tests & improve api performance
Signed-off-by: Henry Gressmann <[email protected]>
1 parent 391c580 commit 0459cc8

File tree

23 files changed

+304
-107
lines changed

23 files changed

+304
-107
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,14 @@ Since this is not a library, this changelog focuses on the changes that are rele
2727

2828
### 🐛 Bug Fixes
2929

30-
- Added more tests that fixed some smaller bugs
3130
- Fixed a potential panic when entities are not found in the database ([`31405a72`](https://github.com/explodingcamera/liwan/commit/31405a721dc5c5493098e211927281cca7816fec))
3231
- Fixed issues with the `Yesterday` Date Range ([`76278b57`](https://github.com/explodingcamera/liwan/commit/76278b579c5fe1557bf1c184542ed6ed2aba57cd))
3332

3433
### Other
3534

3635
- Removed Sessions and Average Views per Session metrics. They were not accurate and were removed to avoid confusion.
36+
- Added more tests & improved API performance
37+
- Updated dependencies
3738

3839
## **Liwan v0.1.1** - 2024-09-24
3940

Cargo.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ cookie={version="*"}
8585
[features]
8686
default=["geoip"]
8787
geoip=["dep:maxminddb"]
88+
_enable_seeding=[]
8889

8990
[profile.dev]
9091
opt-level=1
@@ -93,3 +94,8 @@ incremental=true
9394
[profile.release]
9495
lto="thin"
9596
strip=true
97+
98+
[profile.dev.package.duckdb]
99+
inherits="release"
100+
[profile.dev.package.rusqlite]
101+
inherits="release"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<br/>
22

33
<div align="center">
4-
<a href=""><img src="./web/public/favicon.svg" width="100px"></a>
54
<h2>
5+
<img float="left" src="./web/public/favicon.svg" width="16px"/>
66
<a href="https://liwan.dev">liwan.dev</a> - Easy & Privacy-First Web Analytics
77
</h2>
88
<div>

src/app/core/reports.rs

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use duckdb::params_from_iter;
77
use eyre::{bail, Result};
88
use poem_openapi::{Enum, Object};
99

10-
#[derive(Object)]
10+
#[derive(Object, Debug, Clone)]
1111
pub struct DateRange {
1212
pub start: time::OffsetDateTime,
1313
pub end: time::OffsetDateTime,
@@ -83,7 +83,7 @@ pub struct ReportStats {
8383
pub avg_time_on_site: f64,
8484
}
8585

86-
#[derive(Object, Debug)]
86+
#[derive(Object, Debug, Clone)]
8787
#[oai(rename_all = "camelCase")]
8888
pub struct DimensionFilter {
8989
/// The dimension to filter by
@@ -195,17 +195,9 @@ fn metric_sql(metric: Metric) -> String {
195195
// bounce sessions: time to next / time to prev are both null or both > 1800
196196
"--sql
197197
count(distinct sd.visitor_id)
198-
filter (
199-
where
200-
(sd.time_until_next_event is null or sd.time_until_next_event > 1800) and
201-
(sd.time_since_previous_event is null or sd.time_since_previous_event > 1800)
202-
)
203-
/
204-
count(distinct sd.visitor_id)
205-
filter (
206-
where
207-
sd.time_until_next_event is null or sd.time_until_next_event > 1800
208-
)
198+
filter (where (sd.time_until_next_event is null or sd.time_until_next_event > 1800) and
199+
(sd.time_since_previous_event is null or sd.time_since_previous_event > 1800)) /
200+
count(distinct sd.visitor_id) filter (where sd.time_until_next_event is null or sd.time_until_next_event > 1800)
209201
"
210202
}
211203
Metric::AvgTimeOnSite => {
@@ -311,8 +303,7 @@ pub fn overall_report(
311303
from events, params
312304
where
313305
event = ?::text and
314-
created_at >= params.start_time and
315-
created_at <= params.end_time and
306+
created_at between params.start_time and params.end_time and
316307
entity_id in ({entity_vars})
317308
{filters_sql}
318309
),
@@ -365,8 +356,6 @@ pub fn overall_stats(
365356
return Ok(ReportStats::default());
366357
}
367358

368-
let mut params = ParamVec::new();
369-
370359
let entity_vars = repeat_vars(entities.len());
371360
let (filters_sql, filters_params) = filter_sql(filters)?;
372361

@@ -375,6 +364,7 @@ pub fn overall_stats(
375364
let metric_bounce_rate = metric_sql(Metric::BounceRate);
376365
let metric_avg_time_on_site = metric_sql(Metric::AvgTimeOnSite);
377366

367+
let mut params = ParamVec::new();
378368
params.push(range.start);
379369
params.push(range.end);
380370
params.push(event);
@@ -399,11 +389,10 @@ pub fn overall_stats(
399389
from events, params
400390
where
401391
event = ?::text and
402-
created_at >= params.start_time and
403-
created_at <= params.end_time and
392+
created_at between params.start_time and params.end_time and
404393
entity_id in ({entity_vars})
405394
{filters_sql}
406-
)
395+
)
407396
select
408397
{metric_total} as total_views,
409398
{metric_unique_visitors} as unique_visitors,
@@ -485,10 +474,9 @@ pub fn dimension_report(
485474
extract(epoch from (created_at - lag(created_at) over (partition by visitor_id order by created_at))) as time_since_previous_event
486475
from events sd, params
487476
where
488-
sd.event = ?::text and
489-
sd.created_at >= params.start_time and
490-
sd.created_at <= params.end_time and
491-
sd.entity_id in ({entity_vars})
477+
event = ?::text and
478+
created_at between params.start_time and params.end_time and
479+
entity_id in ({entity_vars})
492480
{filters_sql}
493481
group by
494482
{group_by_columns}, visitor_id, created_at

src/app/core/sessions.rs

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
use crate::app::SqlitePool;
1+
use crate::app::{models, SqlitePool};
22
use eyre::Result;
3+
use rusqlite::params;
34
use time::OffsetDateTime;
45

56
#[derive(Clone)]
@@ -20,17 +21,40 @@ impl LiwanSessions {
2021
Ok(())
2122
}
2223

23-
/// Get the username associated with a session ID, if the session is still valid.
24+
/// Get the user associated with a session ID, if the session is still valid.
2425
/// Returns None if the session is expired
25-
pub fn get(&self, session_id: &str) -> Result<Option<String>> {
26+
pub fn get(&self, session_id: &str) -> Result<Option<models::User>> {
2627
let conn = self.pool.get()?;
27-
let mut stmt = conn.prepare_cached("select username, expires_at from sessions where id = ?")?;
28-
let (username, expires_at): (String, OffsetDateTime) =
29-
stmt.query_row([session_id], |row| Ok((row.get("username")?, row.get("expires_at")?)))?;
30-
if expires_at < OffsetDateTime::now_utc() {
31-
return Ok(None);
32-
}
33-
Ok(Some(username))
28+
29+
let mut stmt = conn.prepare_cached(
30+
r#"--sql
31+
select u.username, u.role, u.projects
32+
from sessions s
33+
join users u
34+
on lower(u.username) = lower(s.username)
35+
where
36+
s.id = ?
37+
and s.expires_at > ?
38+
"#,
39+
)?;
40+
41+
let user = stmt.query_row(params![session_id, time::OffsetDateTime::now_utc()], |row| {
42+
Ok(models::User {
43+
username: row.get("username")?,
44+
role: row.get::<_, String>("role")?.try_into().unwrap_or_default(),
45+
projects: row.get::<_, String>("projects")?.split(',').map(str::to_string).collect(),
46+
})
47+
});
48+
49+
user.map(Some).or_else(
50+
|err| {
51+
if err == rusqlite::Error::QueryReturnedNoRows {
52+
Ok(None)
53+
} else {
54+
Err(err.into())
55+
}
56+
},
57+
)
3458
}
3559

3660
/// Delete a session

src/app/mod.rs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,10 @@ impl Liwan {
102102
}
103103
}
104104

105-
#[cfg(debug_assertions)]
105+
#[cfg(any(debug_assertions, test, feature = "_enable_seeding"))]
106106
impl Liwan {
107-
pub fn seed_database(&self) -> Result<()> {
107+
pub fn seed_database(&self, count_per_entity: usize) -> Result<()> {
108108
use models::UserRole;
109-
use rand::Rng;
110109
use time::OffsetDateTime;
111110

112111
let entities = vec![
@@ -140,13 +139,10 @@ impl Liwan {
140139
&models::Entity { id: entity_id.to_string(), display_name: display_name.to_string() },
141140
&project_ids,
142141
)?;
143-
let events = crate::utils::seed::random_events(
144-
(start, end),
145-
entity_id,
146-
fqdn,
147-
rand::thread_rng().gen_range(5000..20000),
148-
);
142+
let events = crate::utils::seed::random_events((start, end), entity_id, fqdn, count_per_entity);
143+
let now = std::time::Instant::now();
149144
self.events.append(events)?;
145+
tracing::info!("Seeded entity {} in {:?}", entity_id, now.elapsed());
150146
}
151147

152148
Ok(())

src/cli.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ pub enum Command {
3232
UpdatePassword(UpdatePassword),
3333
AddUser(AddUser),
3434
Users(ListUsers),
35-
#[cfg(debug_assertions)]
35+
#[cfg(any(debug_assertions, test, feature = "_enable_seeding"))]
3636
SeedDatabase(SeedDatabase),
3737
}
3838

39-
#[cfg(debug_assertions)]
39+
#[cfg(any(debug_assertions, test, feature = "_enable_seeding"))]
4040
#[derive(FromArgs)]
4141
#[argh(subcommand, name = "seed-database")]
4242
/// Seed the database with some test data
@@ -130,10 +130,10 @@ pub fn handle_command(mut config: Config, cmd: Command) -> Result<()> {
130130
std::fs::write(&output, DEFAULT_CONFIG)?;
131131
println!("Configuration file written to liwan.config.toml");
132132
}
133-
#[cfg(debug_assertions)]
133+
#[cfg(any(debug_assertions, test, feature = "_enable_seeding"))]
134134
Command::SeedDatabase(_) => {
135135
let app = Liwan::try_new(config)?;
136-
app.seed_database()?;
136+
app.seed_database(100000)?;
137137
println!("Database seeded with test data");
138138
}
139139
}

src/utils/seed.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#![allow(dead_code)]
21
use rand::Rng;
32
use time::OffsetDateTime;
43

@@ -44,6 +43,12 @@ pub fn random_events(
4443
generated += 1;
4544

4645
let created_at = random_date(time_range.0, time_range.1, 0.5);
46+
47+
// let time_slice = time_range.1 - time_range.0;
48+
// let skew_factor = 2.0;
49+
// let normalized = 1.0 - (1.0 - (generated as f64 / count as f64)).powf(skew_factor);
50+
// let created_at = time_range.0 + time_slice * normalized;
51+
4752
let path = random_el(PATHS, 0.5);
4853
let referrer = random_el(REFERRERS, 0.5);
4954
let platform = random_el(PLATFORMS, -0.5);

src/web/routes/admin.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,10 @@ impl AdminAPI {
220220
http_bail!(StatusCode::FORBIDDEN, "Forbidden")
221221
}
222222

223-
app.users
224-
.create(&user.username, &user.password, user.role, &[])
223+
let app = app.clone();
224+
tokio::task::spawn_blocking(move || app.users.create(&user.username, &user.password, user.role, &[]))
225+
.await
226+
.http_err("Failed to create user", StatusCode::INTERNAL_SERVER_ERROR)?
225227
.http_err("Failed to create user", StatusCode::INTERNAL_SERVER_ERROR)?;
226228

227229
EmptyResponse::ok()

0 commit comments

Comments
 (0)