use reqwest::Method;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{client::Sendry, error::Error, Page};
#[derive(Debug, Clone)]
pub struct Analytics {
client: Sendry,
}
impl Analytics {
pub(crate) fn new(client: Sendry) -> Self {
Self { client }
}
pub async fn stats(&self, params: AnalyticsParams) -> Result<AnalyticsResponse, Error> {
let q = params.to_query();
self.client
.request(
self.client
.build::<()>(Method::GET, "/v1/analytics", &q, None),
)
.await
}
pub async fn logs(&self, params: LogsParams) -> Result<Page<LogEvent>, Error> {
let q = params.to_query();
self.client
.request(self.client.build::<()>(Method::GET, "/v1/logs", &q, None))
.await
}
pub async fn cohorts(&self, params: CohortParams) -> Result<CohortResponse, Error> {
let q = params.to_query();
self.client
.request(
self.client
.build::<()>(Method::GET, "/v1/analytics/cohorts", &q, None),
)
.await
}
pub async fn benchmarks(&self, params: BenchmarkParams) -> Result<BenchmarkResponse, Error> {
let q = params.to_query();
self.client
.request(self.client.build::<()>(
Method::GET,
"/v1/analytics/benchmarks",
&q,
None,
))
.await
}
pub async fn toggle_benchmark_opt_in(&self, opt_in: bool) -> Result<Value, Error> {
#[derive(Serialize)]
struct Body {
opt_in: bool,
}
self.client
.request(self.client.build(
Method::POST,
"/v1/analytics/benchmarks/opt-in",
&[],
Some(&Body { opt_in }),
))
.await
}
pub async fn breakdowns(&self, params: BreakdownParams) -> Result<BreakdownResponse, Error> {
let q = params.to_query();
self.client
.request(self.client.build::<()>(
Method::GET,
"/v1/analytics/breakdowns",
&q,
None,
))
.await
}
pub async fn comparison(&self, params: AnalyticsParams) -> Result<ComparisonResponse, Error> {
let q = params.to_query();
self.client
.request(self.client.build::<()>(
Method::GET,
"/v1/analytics/comparison",
&q,
None,
))
.await
}
pub async fn export(&self, params: ExportParams) -> Result<Value, Error> {
let q = params.to_query();
self.client
.request(
self.client
.build::<()>(Method::GET, "/v1/analytics/export", &q, None),
)
.await
}
}
#[derive(Debug, Clone, Default)]
pub struct AnalyticsParams {
pub from: String,
pub to: String,
pub granularity: Option<String>,
pub tag: Option<String>,
pub domain: Option<String>,
}
impl AnalyticsParams {
fn to_query(&self) -> Vec<(&'static str, String)> {
let mut q = vec![("from", self.from.clone()), ("to", self.to.clone())];
if let Some(v) = &self.granularity {
q.push(("granularity", v.clone()));
}
if let Some(v) = &self.tag {
q.push(("tag", v.clone()));
}
if let Some(v) = &self.domain {
q.push(("domain", v.clone()));
}
q
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct AnalyticsBucket {
pub date: String,
pub sent: u64,
pub delivered: u64,
pub opened: u64,
pub clicked: u64,
pub bounced: u64,
pub complained: u64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct AnalyticsSummary {
pub sent: u64,
pub delivered: u64,
pub opened: u64,
pub clicked: u64,
pub bounced: u64,
pub complained: u64,
pub delivery_rate: f64,
pub open_rate: f64,
pub click_rate: f64,
pub bounce_rate: f64,
pub complaint_rate: f64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct AnalyticsResponse {
pub summary: AnalyticsSummary,
pub timeseries: Vec<AnalyticsBucket>,
}
#[derive(Debug, Clone, Default)]
pub struct LogsParams {
pub limit: Option<u32>,
pub cursor: Option<String>,
pub email_id: Option<String>,
pub event_type: Option<String>,
pub to: Option<String>,
pub from_date: Option<String>,
pub to_date: Option<String>,
}
impl LogsParams {
fn to_query(&self) -> Vec<(&'static str, String)> {
let mut q = Vec::new();
if let Some(v) = self.limit {
q.push(("limit", v.to_string()));
}
if let Some(v) = &self.cursor {
q.push(("cursor", v.clone()));
}
if let Some(v) = &self.email_id {
q.push(("email_id", v.clone()));
}
if let Some(v) = &self.event_type {
q.push(("type", v.clone()));
}
if let Some(v) = &self.to {
q.push(("to", v.clone()));
}
if let Some(v) = &self.from_date {
q.push(("from_date", v.clone()));
}
if let Some(v) = &self.to_date {
q.push(("to_date", v.clone()));
}
q
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct LogEvent {
pub id: String,
pub email_id: String,
#[serde(rename = "type")]
pub event_type: String,
pub recipient: String,
pub metadata: Option<Value>,
pub created_at: String,
}
#[derive(Debug, Clone, Default)]
pub struct CohortParams {
pub from: String,
pub to: String,
pub granularity: Option<String>,
pub metric: Option<String>,
}
impl CohortParams {
fn to_query(&self) -> Vec<(&'static str, String)> {
let mut q = vec![("from", self.from.clone()), ("to", self.to.clone())];
if let Some(v) = &self.granularity {
q.push(("granularity", v.clone()));
}
if let Some(v) = &self.metric {
q.push(("metric", v.clone()));
}
q
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct CohortBucket {
pub cohort_date: String,
pub period_offset: i32,
pub total_sent: u64,
pub metric_value: f64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CohortResponse {
pub cohorts: Vec<CohortBucket>,
pub granularity: String,
pub metric: String,
}
#[derive(Debug, Clone, Default)]
pub struct BenchmarkParams {
pub from: String,
pub to: String,
pub granularity: Option<String>,
}
impl BenchmarkParams {
fn to_query(&self) -> Vec<(&'static str, String)> {
let mut q = vec![("from", self.from.clone()), ("to", self.to.clone())];
if let Some(v) = &self.granularity {
q.push(("granularity", v.clone()));
}
q
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct BenchmarkBucket {
pub date: String,
pub your_delivery_rate: f64,
pub your_open_rate: f64,
pub your_click_rate: f64,
pub avg_delivery_rate: f64,
pub avg_open_rate: f64,
pub avg_click_rate: f64,
pub p50_delivery_rate: f64,
pub p50_open_rate: f64,
pub p50_click_rate: f64,
pub p75_delivery_rate: f64,
pub p75_open_rate: f64,
pub p75_click_rate: f64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BenchmarkResponse {
pub data: Vec<BenchmarkBucket>,
pub benchmark_opt_in: bool,
pub org_count: u32,
}
#[derive(Debug, Clone, Default)]
pub struct BreakdownParams {
pub from: String,
pub to: String,
pub group_by: String,
pub limit: Option<u32>,
}
impl BreakdownParams {
fn to_query(&self) -> Vec<(&'static str, String)> {
let mut q = vec![
("from", self.from.clone()),
("to", self.to.clone()),
("group_by", self.group_by.clone()),
];
if let Some(v) = self.limit {
q.push(("limit", v.to_string()));
}
q
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct BreakdownItem {
pub id: Option<String>,
pub name: Option<String>,
pub sent: u64,
pub delivered: u64,
pub opened: u64,
pub clicked: u64,
pub bounced: u64,
pub complained: u64,
pub delivery_rate: f64,
pub open_rate: f64,
pub click_rate: f64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BreakdownResponse {
pub data: Vec<BreakdownItem>,
pub group_by: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ComparisonPeriodStats {
pub sent: u64,
pub delivered: u64,
pub opened: u64,
pub clicked: u64,
pub bounced: u64,
pub complained: u64,
pub delivery_rate: f64,
pub open_rate: f64,
pub click_rate: f64,
pub bounce_rate: f64,
pub complaint_rate: f64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ComparisonChanges {
pub sent_pct: f64,
pub delivered_pct: f64,
pub opened_pct: f64,
pub clicked_pct: f64,
pub bounced_pct: f64,
pub complained_pct: f64,
pub delivery_rate_delta: f64,
pub open_rate_delta: f64,
pub click_rate_delta: f64,
pub bounce_rate_delta: f64,
pub complaint_rate_delta: f64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ComparisonResponse {
pub current: ComparisonPeriodStats,
pub previous: ComparisonPeriodStats,
pub changes: ComparisonChanges,
}
#[derive(Debug, Clone, Default)]
pub struct ExportParams {
pub from: String,
pub to: String,
pub granularity: Option<String>,
pub format: Option<String>,
pub domain: Option<String>,
}
impl ExportParams {
fn to_query(&self) -> Vec<(&'static str, String)> {
let mut q = vec![("from", self.from.clone()), ("to", self.to.clone())];
if let Some(v) = &self.granularity {
q.push(("granularity", v.clone()));
}
if let Some(v) = &self.format {
q.push(("format", v.clone()));
}
if let Some(v) = &self.domain {
q.push(("domain", v.clone()));
}
q
}
}