matomo-rs 0.1.0

Async client for the Matomo Reporting API, focused on data export and migration
Documentation
use crate::de::empty_or_vec;
use serde::Deserialize;
use serde_with::{serde_as, DisplayFromStr, PickFirst};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum VisitorType {
    New,
    Returning,
    #[serde(other)]
    Unknown,
}

/// A single visit from `Live.getLastVisitsDetails`.
///
/// Only data-bearing fields are modeled; Matomo's presentation fields
/// (`*Pretty`, `*Icon`, `*IconSVG`, flag images, etc.) are silently dropped by
/// not declaring them. `deny_unknown_fields` is deliberately NOT used.
#[serde_as]
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Visit {
    // ids
    pub id_visit: i64,
    #[serde(default)]
    pub visitor_id: Option<String>,
    #[serde(default)]
    pub fingerprint: Option<String>,
    #[serde(default)]
    pub user_id: Option<String>,

    // timestamps (unix seconds)
    #[serde(default)]
    pub server_timestamp: Option<i64>,
    #[serde(default)]
    pub first_action_timestamp: Option<i64>,
    #[serde(default)]
    pub last_action_timestamp: Option<i64>,

    // visit metrics
    #[serde(default)]
    pub visit_count: i64,
    #[serde(default)]
    pub visit_duration: i64,
    #[serde(default)]
    pub actions: i64,
    #[serde(default)]
    pub searches: i64,
    #[serde(default)]
    pub interactions: i64,
    #[serde(default)]
    pub visitor_type: Option<VisitorType>,
    #[serde_as(as = "PickFirst<(_, DisplayFromStr)>")]
    #[serde(default)]
    pub visit_converted: i64,

    // referrer
    #[serde(default)]
    pub referrer_type: Option<String>,
    #[serde(default)]
    pub referrer_name: Option<String>,
    #[serde(default)]
    pub referrer_keyword: Option<String>,
    #[serde(default)]
    pub referrer_url: Option<String>,

    // geo
    #[serde(default)]
    pub continent: Option<String>,
    #[serde(default)]
    pub country: Option<String>,
    #[serde(default)]
    pub country_code: Option<String>,
    #[serde(default)]
    pub region: Option<String>,
    #[serde(default)]
    pub city: Option<String>,
    /// Matomo sends these as strings.
    #[serde_as(as = "Option<PickFirst<(DisplayFromStr, _)>>")]
    #[serde(default)]
    pub latitude: Option<f64>,
    #[serde_as(as = "Option<PickFirst<(DisplayFromStr, _)>>")]
    #[serde(default)]
    pub longitude: Option<f64>,

    // device / os / browser
    #[serde(default)]
    pub device_type: Option<String>,
    #[serde(default)]
    pub device_brand: Option<String>,
    #[serde(default)]
    pub device_model: Option<String>,
    #[serde(default)]
    pub operating_system_name: Option<String>,
    #[serde(default)]
    pub browser_name: Option<String>,
    #[serde(default)]
    pub browser_version: Option<String>,
    #[serde(default)]
    pub resolution: Option<String>,

    #[serde(default)]
    pub action_details: Vec<ActionDetail>,

    /// Integer `0` when there are no conversions, an array otherwise.
    #[serde(default, deserialize_with = "empty_or_vec")]
    pub goal_conversions: Vec<GoalConversion>,
}

/// A goal conversion attached to a visit.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GoalConversion {
    #[serde(default)]
    pub goal_id: Option<i64>,
    #[serde(default)]
    pub revenue: Option<f64>,
    #[serde(default)]
    pub url: Option<String>,
    #[serde(default)]
    pub timestamp: Option<i64>,
}

/// An entry in `actionDetails`. Internally tagged on Matomo's `type` field.
/// `action` and `download` are typed precisely from harvested data; the others
/// are modeled minimally; anything else falls through to `Unknown` for
/// forward-compat. Never `#[serde(untagged)]`.
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ActionDetail {
    Action(ActionPageview),
    Download(ActionLink),
    Outlink(ActionLink),
    Event(ActionEvent),
    Search(ActionSearch),
    Goal(ActionGoal),
    #[serde(other)]
    Unknown,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ActionPageview {
    #[serde(default)]
    pub url: Option<String>,
    #[serde(default)]
    pub page_title: Option<String>,
    #[serde(default)]
    pub page_id_action: Option<i64>,
    #[serde(default)]
    pub page_id: Option<i64>,
    #[serde(default)]
    pub idpageview: Option<String>,
    #[serde(default)]
    pub time_spent: Option<i64>,
    #[serde(default)]
    pub pageview_position: Option<i64>,
    #[serde(default)]
    pub timestamp: Option<i64>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ActionLink {
    #[serde(default)]
    pub url: Option<String>,
    #[serde(default)]
    pub page_title: Option<String>,
    #[serde(default)]
    pub page_id_action: Option<i64>,
    #[serde(default)]
    pub page_id: Option<i64>,
    #[serde(default)]
    pub idpageview: Option<String>,
    #[serde(default)]
    pub timestamp: Option<i64>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ActionEvent {
    #[serde(default)]
    pub event_category: Option<String>,
    #[serde(default)]
    pub event_action: Option<String>,
    #[serde(default)]
    pub event_name: Option<String>,
    #[serde(default)]
    pub timestamp: Option<i64>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ActionSearch {
    #[serde(default)]
    pub keyword: Option<String>,
    #[serde(default)]
    pub timestamp: Option<i64>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ActionGoal {
    #[serde(default)]
    pub goal_id: Option<i64>,
    #[serde(default)]
    pub revenue: Option<f64>,
    #[serde(default)]
    pub timestamp: Option<i64>,
}