wakapi 0.3.1

Wakatime API client
Documentation
//! Heartbeats endpoint
//!
//! - Ref: <https://wakatime.com/developers#heartbeats>
//! - Last checked : 2026-05-15
//!
//! WARNING: This endpoint is GET and POST, but only GET is implemented for now.
//!

use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize};

use crate::{client::WakapiClient, error::WakapiError};

/// Request parameters for the Heartbeats endpoint.
///
/// Ref: <https://wakatime.com/developers#heartbeats>
#[derive(Serialize, Default)]
pub struct HeartbeatsParams {
    /// required - Requested day; Heartbeats will be returned from 12am until 11:59pm in user's timezone for this day.
    date: NaiveDate,
}

impl HeartbeatsParams {
    /// Create a new HeartbeatsParams with the given date object (NaiveDate).
    pub fn new(date: NaiveDate) -> HeartbeatsParams {
        HeartbeatsParams { date }
    }

    /// Create a new HeartbeatsParams with the given date string.
    /// The date string should be in the format "YYYY-MM-DD".
    pub fn from_date_str(date: &str) -> Result<HeartbeatsParams, chrono::ParseError> {
        Ok(HeartbeatsParams {
            date: NaiveDate::parse_from_str(date, "%Y-%m-%d")?,
        })
    }
}

/// Heartbeats endpoint
///
/// Ref: <https://wakatime.com/developers#heartbeats>
///
#[derive(Deserialize, Debug)]
pub struct Heartbeats {
    /// List of heartbeats for the requested day.
    pub data: Vec<Heartbeat>,
    /// Start of time range as ISO 8601 UTC datetime
    pub start: DateTime<Utc>,
    /// End of time range as ISO 8601 UTC datetime
    pub end: DateTime<Utc>,
    /// timezone used for this request in Olson Country/Region format
    pub timezone: String,
}

/// Heartbeat item
#[derive(Deserialize, Debug)]
pub struct Heartbeat {
    /// Entity heartbeat is logging time against, such as an absolute file path or domain
    pub entity: String,
    /// Type of entity; can be file, app, url, or domain
    #[serde(rename = "type")]
    pub entity_type: String,
    /// Category for this activity; can be coding, building, indexing, debugging, browsing, running tests, writing tests, manual testing, writing docs, code reviewing, communicating, notes, researching, learning, designing, or ai coding
    pub category: String,
    /// UNIX epoch timestamp; numbers after decimal point are fractions of a second
    pub time: f64,
    /// Project name (nullable)
    pub project: Option<String>,
    /// Count of the number of folders in the project root path (nullable)
    pub project_root_count: Option<usize>,
    /// Branch name (nullable)
    pub branch: Option<String>,
    /// Language name (nullable)
    pub language: Option<String>,
    /// Comma separated list of dependencies detected from entity file (nullable)
    pub dependencies: Option<String>,
    /// Unique id of the machine which generated this coding activity
    pub machine_name_id: String,
    /// Number of lines added or removed by GenAI since last heartbeat in the current file (nullable)
    pub ai_line_changes: Option<i64>,
    /// Number of lines added or removed by old-school typing since last heartbeat in the current file (nullable)
    pub human_line_changes: Option<i64>,
    /// AI session id (nullable)
    pub ai_session: Option<String>,
    /// Number of user input tokens used since the last heartbeat by GenAI tools
    pub ai_input_tokens: Option<i64>,
    /// Number of output tokens used since the last heartbeat by GenAI tools
    pub ai_output_tokens: Option<i64>,
    /// Number of user prompt characters typed to AI since the last heartbeat
    pub ai_prompt_length: Option<i64>,
    /// Subscription plan for the GenAI tool used for this heartbeat (nullable)
    pub ai_subscription_plan: Option<String>,
    /// Total number of lines in the entity when entity type is a file (nullable)
    pub lines: Option<usize>,
    /// Current line row number of cursor (nullable)
    pub lineno: Option<usize>,
    /// Current cursor column position (nullable)
    pub cursorpos: Option<usize>,
    /// Whether this heartbeat was triggered from writing to a file
    pub is_write: bool,
}

impl Heartbeats {
    #[cfg(feature = "blocking")]

    /// Get heartbeats for the given day.
    pub fn fetch(client: &WakapiClient, params: HeartbeatsParams) -> Result<Self, WakapiError> {
        let url = client.build_url(
            "/api/v1/users/:user/heartbeats",
            Some(serde_url_params::to_string(&params)?),
        );
        // // Debug : print url and body text
        // println!(
        //     "url: {}\nbody: {}",
        //     url,
        //     reqwest::blocking::Client::new()
        //         .get(&url)
        //         .header("Authorization", client.get_auth_header())
        //         .send()?
        //         .text()?
        // );
        let response = reqwest::blocking::Client::new()
            .get(&url)
            .header("Authorization", client.get_auth_header())
            .send()?;
        if response.status().is_success() {
            let body = response.json::<Heartbeats>()?;
            Ok(body)
        } else {
            let error = response.json::<crate::error::ErrorMessage>()?;
            Err(WakapiError::ResponseError(error))
        }
    }

    #[cfg(not(feature = "blocking"))]
    pub async fn fetch(
        client: &WakapiClient,
        params: HeartbeatsParams,
    ) -> Result<Self, WakapiError> {
        let url = client.build_url(
            "/api/v1/users/:user/heartbeats",
            Some(serde_url_params::to_string(&params)?),
        );
        // // Debug : print url and body text
        // println!(
        //     "url: {}\nbody: {}",
        //     url,
        //     reqwest::Client::new()
        //         .get(&url)
        //         .header("Authorization", client.get_auth_header())
        //         .send()
        //         .await?
        //         .text()
        // );
        let response = reqwest::Client::new()
            .get(&url)
            .header("Authorization", client.get_auth_header())
            .send()
            .await?;
        if response.status().is_success() {
            let body = response.json::<Heartbeats>().await?;
            Ok(body)
        } else {
            let error = response.json::<crate::error::ErrorMessage>().await?;
            Err(WakapiError::ResponseError(error))
        }
    }
}