Skip to main content

runlight/store/
rows.rs

1//! What the store keeps and gives back, in the SDK's shapes. `to_value`
2//! writes each as the SDK's object, keys in its order.
3
4use crate::js::{Object, Value};
5use crate::obj;
6
7/// A site.
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct SiteRow {
10    /// Stable id, stored with every row.
11    pub id: String,
12    /// Its name.
13    pub name: String,
14    /// Hostnames that belong to it, without www.
15    pub hostnames: Vec<String>,
16    /// IANA timezone for reports.
17    pub timezone: String,
18}
19
20impl SiteRow {
21    /// `{ id, name, hostnames, timezone }`.
22    pub fn to_value(&self) -> Value {
23        obj! {
24            "id" => self.id.clone(),
25            "name" => self.name.clone(),
26            "hostnames" => Value::Array(self.hostnames.iter().map(|h| Value::from(h.as_str())).collect()),
27            "timezone" => self.timezone.clone(),
28        }
29    }
30}
31
32/// What the dashboard may change about a site, kept as the JSON object it
33/// was saved as (`{ name?, timezone? }`).
34pub type SiteOverrides = Object;
35
36/// Something worth counting.
37#[derive(Clone, Debug, PartialEq)]
38pub struct GoalRow {
39    /// Its id.
40    pub id: String,
41    /// The site's id.
42    pub site: String,
43    /// Its name.
44    pub name: String,
45    /// `event`, `page`, or `click`.
46    pub kind: String,
47    /// The event name, the path pattern, or for a click goal a CSS selector or URL pattern.
48    pub match_: String,
49    /// For click goals: `selector` or `link`; else empty.
50    pub click_by: String,
51    /// `none`, `fixed`, or `prop`.
52    pub value_mode: String,
53    /// The fixed amount.
54    pub value: f64,
55    /// The event property holding the amount.
56    pub value_prop: String,
57    /// The currency.
58    pub currency: String,
59    /// When it was made.
60    pub created_at: i64,
61}
62
63impl GoalRow {
64    /// The SDK's object.
65    pub fn to_value(&self) -> Value {
66        obj! {
67            "id" => self.id.clone(), "site" => self.site.clone(), "name" => self.name.clone(), "kind" => self.kind.clone(),
68            "match" => self.match_.clone(), "clickBy" => self.click_by.clone(), "valueMode" => self.value_mode.clone(),
69            "value" => self.value, "valueProp" => self.value_prop.clone(), "currency" => self.currency.clone(),
70            "createdAt" => self.created_at,
71        }
72    }
73}
74
75/// Someone who gets a site's report by email. `token` is the unsubscribe key.
76#[derive(Clone, Debug, PartialEq, Eq)]
77pub struct ReportRow {
78    /// Its id.
79    pub id: String,
80    /// The site's id.
81    pub site: String,
82    /// Where it goes.
83    pub email: String,
84    /// `weekly` or `monthly`.
85    pub frequency: String,
86    /// Its language.
87    pub lang: String,
88    /// The unsubscribe key.
89    pub token: String,
90    /// Where the dashboard lives, for the links in the email.
91    pub origin: String,
92    /// The last period sent, like w:2026-09-28 or m:2026-09, so nothing goes out twice.
93    pub last_period: String,
94    /// When it was last sent.
95    pub last_sent_at: Option<i64>,
96    /// When it was made.
97    pub created_at: i64,
98}
99
100impl ReportRow {
101    /// The SDK's object.
102    pub fn to_value(&self) -> Value {
103        obj! {
104            "id" => self.id.clone(), "site" => self.site.clone(), "email" => self.email.clone(), "frequency" => self.frequency.clone(),
105            "lang" => self.lang.clone(), "token" => self.token.clone(), "origin" => self.origin.clone(),
106            "lastPeriod" => self.last_period.clone(), "lastSentAt" => self.last_sent_at, "createdAt" => self.created_at,
107        }
108    }
109}
110
111/// A goal's totals.
112#[derive(Clone, Copy, Debug, Default, PartialEq)]
113pub struct GoalTotals {
114    /// Rows that converted.
115    pub conversions: f64,
116    /// Visitors who converted.
117    pub visitors: f64,
118    /// Money, to the cent.
119    pub revenue: f64,
120}
121
122/// A public, read-only view of one site's stats, opened by its unguessable id.
123#[derive(Clone, Debug, PartialEq, Eq)]
124pub struct ShareRow {
125    /// Its id.
126    pub id: String,
127    /// The site's id.
128    pub site: String,
129    /// Its name.
130    pub name: String,
131    /// When it was made.
132    pub created_at: i64,
133}
134
135impl ShareRow {
136    /// The SDK's object.
137    pub fn to_value(&self) -> Value {
138        obj! { "id" => self.id.clone(), "site" => self.site.clone(), "name" => self.name.clone(), "createdAt" => self.created_at }
139    }
140}
141
142/// One step of a funnel: reaching a page (with * as a wildcard), or sending an event.
143#[derive(Clone, Debug, PartialEq, Eq)]
144pub struct FunnelStep {
145    /// `page` or `event`.
146    pub kind: String,
147    /// The page or event.
148    pub match_: String,
149}
150
151/// Steps a visit is expected to take in order.
152#[derive(Clone, Debug, PartialEq, Eq)]
153pub struct FunnelRow {
154    /// Its id.
155    pub id: String,
156    /// The site's id.
157    pub site: String,
158    /// Its name.
159    pub name: String,
160    /// Its steps.
161    pub steps: Vec<FunnelStep>,
162    /// When it was made.
163    pub created_at: i64,
164}
165
166impl FunnelRow {
167    /// The steps as stored: `[{ kind, match }]`.
168    pub fn steps_value(&self) -> Value {
169        Value::Array(
170            self.steps.iter().map(|s| obj! { "kind" => s.kind.clone(), "match" => s.match_.clone() }).collect(),
171        )
172    }
173
174    /// The SDK's object.
175    pub fn to_value(&self) -> Value {
176        obj! { "id" => self.id.clone(), "site" => self.site.clone(), "name" => self.name.clone(), "steps" => self.steps_value(), "createdAt" => self.created_at }
177    }
178}
179
180/// An API token. Only its hash is stored; the token itself is shown once.
181#[derive(Clone, Debug, PartialEq, Eq)]
182pub struct TokenRow {
183    /// Its id.
184    pub id: String,
185    /// Its name.
186    pub name: String,
187    /// "" reads every site; otherwise the one site it may read.
188    pub site: String,
189    /// `read`, `manage`, or `embed` (a CMS plugin's key, which only gets tickets that open its one site's
190    /// read-only dashboard inside the CMS).
191    pub scope: String,
192    /// The token's hash.
193    pub hash: String,
194    /// The token's last four characters.
195    pub hint: String,
196    /// When it was made.
197    pub created_at: i64,
198    /// When it was last used.
199    pub last_used_at: Option<i64>,
200}
201
202/// A short link.
203#[derive(Clone, Debug, PartialEq, Eq)]
204pub struct LinkRow {
205    /// Its id.
206    pub id: String,
207    /// The site's id.
208    pub site: String,
209    /// A custom link domain, or "" for the app's own.
210    pub domain: String,
211    /// Its slug.
212    pub slug: String,
213    /// Its name.
214    pub name: String,
215    /// Where it goes.
216    pub url: String,
217    /// When it was made.
218    pub created_at: i64,
219    /// When it last changed.
220    pub updated_at: i64,
221}
222
223impl LinkRow {
224    /// The SDK's object.
225    pub fn to_object(&self) -> Object {
226        let mut o = Object::new();
227        o.set("id", self.id.clone());
228        o.set("site", self.site.clone());
229        o.set("domain", self.domain.clone());
230        o.set("slug", self.slug.clone());
231        o.set("name", self.name.clone());
232        o.set("url", self.url.clone());
233        o.set("createdAt", self.created_at);
234        o.set("updatedAt", self.updated_at);
235        o
236    }
237}
238
239/// A visit, as recorded from its first request.
240#[derive(Clone, Debug, Default, PartialEq, Eq)]
241pub struct SessionRow {
242    /// Its id.
243    pub id: String,
244    /// The site's id.
245    pub site: String,
246    /// The day's visitor hash.
247    pub visitor: String,
248    /// When it began.
249    pub started_at: i64,
250    /// Its host.
251    pub hostname: String,
252    /// Where it came from.
253    pub referrer_host: String,
254    /// The referrer's path.
255    pub referrer_path: String,
256    /// The source.
257    pub source: String,
258    /// The channel.
259    pub channel: String,
260    /// utm_source.
261    pub utm_source: String,
262    /// utm_medium.
263    pub utm_medium: String,
264    /// utm_campaign.
265    pub utm_campaign: String,
266    /// utm_term.
267    pub utm_term: String,
268    /// utm_content.
269    pub utm_content: String,
270    /// The country.
271    pub country: String,
272    /// The region.
273    pub region: String,
274    /// The city.
275    pub city: String,
276    /// The browser.
277    pub browser: String,
278    /// Its version.
279    pub browser_version: String,
280    /// The system.
281    pub os: String,
282    /// Its version.
283    pub os_version: String,
284    /// The device.
285    pub device: String,
286    /// The screen.
287    pub screen: String,
288    /// The language.
289    pub language: String,
290}
291
292/// One recorded row: a pageview, event, engagement ping, link click, or AI agent fetch.
293#[derive(Clone, Debug, Default, PartialEq)]
294pub struct EventRow {
295    /// The site's id.
296    pub site: String,
297    /// When.
298    pub ts: i64,
299    /// `pageview`, `event`, `engagement`, `click`, or `fetch`.
300    pub kind: String,
301    /// The day's visitor hash.
302    pub visitor: String,
303    /// The visit's id.
304    pub session: String,
305    /// The pageview's id.
306    pub pageview: String,
307    /// The path.
308    pub path: String,
309    /// The host.
310    pub hostname: String,
311    /// The page's title.
312    pub title: String,
313    /// The event's name.
314    pub name: String,
315    /// The event's properties.
316    pub props: Option<Object>,
317    /// Engaged time.
318    pub engaged_ms: i64,
319    /// Deepest scroll.
320    pub scroll: Option<i64>,
321    /// The short link's id.
322    pub link: String,
323}
324
325/// A range's totals.
326#[derive(Clone, Copy, Debug, Default, PartialEq)]
327pub struct Stats {
328    /// Visitors.
329    pub visitors: f64,
330    /// Visits.
331    pub visits: f64,
332    /// Pageviews.
333    pub pageviews: f64,
334    /// Pageviews per visit, to two places.
335    pub views_per_visit: f64,
336    /// 0 to 1.
337    pub bounce_rate: f64,
338    /// Mean engaged time per visit, milliseconds.
339    pub visit_duration: f64,
340}
341
342impl Stats {
343    /// The SDK's object.
344    pub fn to_value(&self) -> Value {
345        obj! {
346            "visitors" => self.visitors, "visits" => self.visits, "pageviews" => self.pageviews,
347            "viewsPerVisit" => self.views_per_visit, "bounceRate" => self.bounce_rate, "visitDuration" => self.visit_duration,
348        }
349    }
350
351    /// A metric by the SDK's name.
352    pub fn get(&self, key: &str) -> f64 {
353        match key {
354            "visitors" => self.visitors,
355            "visits" => self.visits,
356            "pageviews" => self.pageviews,
357            "viewsPerVisit" => self.views_per_visit,
358            "bounceRate" => self.bounce_rate,
359            "visitDuration" => self.visit_duration,
360            _ => 0.0,
361        }
362    }
363}
364
365/// The pageview an engagement ping or event belongs to.
366#[derive(Clone, Debug, PartialEq, Eq)]
367pub struct PageviewInfo {
368    /// The visit's id.
369    pub session: String,
370    /// The visitor.
371    pub visitor: String,
372    /// The path.
373    pub path: String,
374    /// The host.
375    pub hostname: String,
376    /// When.
377    pub ts: i64,
378    /// When its visit began.
379    pub started_at: i64,
380    /// When its visit was last active.
381    pub last_at: i64,
382}