mirror of
https://github.com/modrinth/code.git
synced 2026-08-30 11:36:05 +00:00
wip: dev-1126-v2
This commit is contained in:
Generated
+16
@@ -300,6 +300,22 @@ dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aditude"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"aditude",
|
||||
"arc-swap",
|
||||
"chrono",
|
||||
"dotenvy",
|
||||
"eyre",
|
||||
"reqwest 0.12.24",
|
||||
"rust_decimal",
|
||||
"secrecy",
|
||||
"serde",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
|
||||
@@ -5,6 +5,7 @@ members = [
|
||||
"apps/app-playground",
|
||||
"apps/daedalus_client",
|
||||
"apps/labrinth",
|
||||
"packages/aditude",
|
||||
"packages/app-lib",
|
||||
"packages/ariadne",
|
||||
"packages/daedalus",
|
||||
@@ -55,6 +56,7 @@ aws-sdk-s3 = { version = "=1.122.0", default-features = false, features = [
|
||||
base64 = "0.22.1"
|
||||
bitflags = "2.9.4"
|
||||
bon = "3.9.3"
|
||||
aditude = { path = "packages/aditude" }
|
||||
bytemuck = "1.24.0"
|
||||
bytes = "1.10.1"
|
||||
censor = "0.3.0"
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod billing;
|
||||
pub mod email;
|
||||
pub mod file_scan;
|
||||
pub mod moderation;
|
||||
pub mod payout_run;
|
||||
pub mod payouts;
|
||||
pub mod server_ping;
|
||||
pub mod session;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Logic for fetching and caching revenue estimations from our ad provider.
|
||||
|
||||
pub async fn estimate(aditude: &aditude::Client) {
|
||||
aditude.
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
//! Logic for executing payout runs, including starting a run, and performing
|
||||
//! the revenue distribution.
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
//! ## Payout run
|
||||
//!
|
||||
//! The general flow for a payout run is as follows:
|
||||
//! - For a given month (say, January) our ad provider gives us an estimate of
|
||||
//! how much revenue and how many impressions we received for each individual
|
||||
//! day. The month (payout period) starts in an _open_ state.
|
||||
//! - After NET 60 has passed (start of March), the January payouts _should_ be
|
||||
//! available; usually it takes some time for our ad provider to send the
|
||||
//! money, so this is closer to NET 75. During this period, the month (payout
|
||||
//! period) is in an _in review_ state.
|
||||
//! - Once we receive the money from the provider, an admin enters the total
|
||||
//! amount we've received into the web UI, adds any manual adjustments (for
|
||||
//! campaigns outside of our ad provider's), and starts a payout run.
|
||||
//! - The payout run is not immediately executed; there is a period of time in
|
||||
//! which it can still be cancelled.
|
||||
//! - Once the payout run is executed, we calculate the exact revenue
|
||||
//! distribution to all creators, and fill `payouts_values` with those
|
||||
//! amounts.
|
||||
//!
|
||||
//! ## Distribution
|
||||
//!
|
||||
//! How revenue is distributed:
|
||||
//! - While a month is still open/in review:
|
||||
//! - `raw_estimated_revenue_usd`: how much our ad provider estimates we'll
|
||||
//! earn for a specific period
|
||||
//! - We also get a value for this per day
|
||||
//! - `fees_usd`: how much we pay in fees to Clean.io
|
||||
//! - Based on number of impressions; we can get a per-day value for this
|
||||
//! - `variance_usd`: a fixed percentage that we subtract from the raw estimated
|
||||
//! revenue to account for it being an overestimate
|
||||
//! - e.g. if variance is 10%, and we estimate that we'll earn $100k, then
|
||||
//! our ad provider will probably give us closer to $90k - the variance
|
||||
//! lets us express this difference
|
||||
//! - `net_estimated_revenue_usd`: raw estimated - fees - variance
|
||||
//! - `platform_net_estimated_revenue_usd`: net estimated revenue x Modrinth's cut
|
||||
//! - `creator_net_estimated_revenue_usd`: net estimated revenue x (1 - Modrinth's cut)
|
||||
//! - After a payout run has been executed:
|
||||
//! - We save the per-day raw estimated revenue and impressions in the
|
||||
//! database
|
||||
//! - `raw_actual_revenue_usd`: how much we got from Aditude, input by an admin
|
||||
//! - We compute this per-day by:
|
||||
//! ```text
|
||||
//! let factor = raw_actual_revenue_usd / raw_estimated_revenue_usd
|
||||
//! raw_actual_revenue_usd_for_today = raw_estimated_revenue_usd_for_today * factor
|
||||
//! ```
|
||||
//! - (fees stay the same, since they're based on impressions, not revenue)
|
||||
//! - (variance is ignored, since that's purely an estimation value)
|
||||
//! - `adjustments_usd`: sum of all manual adjustments input by the admin
|
||||
//! - `net_actual_revenue_usd`: raw actual revenue - fees + adjustments
|
||||
//! - `(platform|creator)_net_estimated_revenue_usd`: same logic as estimated,
|
||||
//! but using the net actual revenue
|
||||
//!
|
||||
//! ## Variance
|
||||
//!
|
||||
//! We store a table `payouts_variance` with columns:
|
||||
//! - a timestamp from when this variance value applies (first entry at Unix
|
||||
//! epoch)
|
||||
//! - the decimal fraction of variance to apply
|
||||
|
||||
mod estimate;
|
||||
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "aditude"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
arc-swap = { workspace = true, optional = true }
|
||||
reqwest = { workspace = true, features = ["json"] }
|
||||
rust_decimal = { workspace = true }
|
||||
secrecy = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
aditude = { path = ".", features = ["mock"] }
|
||||
dotenvy = { workspace = true }
|
||||
eyre = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
|
||||
[features]
|
||||
mock = ["dep:arc-swap"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1 @@
|
||||
Types for the [Aditude API](https://aditude.io).
|
||||
@@ -0,0 +1,38 @@
|
||||
//! Example Aditude client.
|
||||
#![expect(clippy::print_stdout, reason = "this is an example")]
|
||||
|
||||
use aditude::{Client, v1, v2};
|
||||
use eyre::{Context, Result};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
_ = dotenvy::dotenv();
|
||||
let aditude = Client::new(
|
||||
dotenvy::var("ADITUDE_API_URL").wrap_err("no API URL")?,
|
||||
dotenvy::var("ADITUDE_API_KEY").wrap_err("no API key")?,
|
||||
);
|
||||
|
||||
let resp = aditude
|
||||
.get_metrics_v1(v1::GetMetrics {
|
||||
metrics: &[v1::MetricKind::Impressions, v1::MetricKind::Revenue],
|
||||
range: v1::Range::Yesterday,
|
||||
interval: v1::Interval::OneDay,
|
||||
})
|
||||
.await
|
||||
.wrap_err("failed to get metrics")?;
|
||||
println!("{resp:#?}");
|
||||
|
||||
println!("\n---\n");
|
||||
|
||||
let resp = aditude
|
||||
.get_metrics_v2(v2::GetMetrics {
|
||||
metrics: &[v2::MetricKind::Impressions, v2::MetricKind::Revenue],
|
||||
range: v2::Range::Yesterday,
|
||||
interval: v2::Interval::OneDay,
|
||||
})
|
||||
.await
|
||||
.wrap_err("failed to get metrics")?;
|
||||
println!("{resp:#?}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![allow(missing_docs, reason = "these are Aditude types")]
|
||||
|
||||
#[cfg(feature = "mock")]
|
||||
pub mod mock;
|
||||
pub mod v1;
|
||||
pub mod v2;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use secrecy::SecretString;
|
||||
|
||||
/// [Aditude](https://www.aditude.com/) client.
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
http: reqwest::Client,
|
||||
pub api_url: Cow<'static, str>,
|
||||
pub api_key: SecretString,
|
||||
#[cfg(feature = "mock")]
|
||||
pub mock: arc_swap::ArcSwapOption<mock::AditudeMock>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Creates a new Aditude client with a [`reqwest::Client`].
|
||||
#[must_use]
|
||||
pub fn from_client(
|
||||
http: reqwest::Client,
|
||||
api_url: impl Into<Cow<'static, str>>,
|
||||
api_key: impl Into<SecretString>,
|
||||
) -> Self {
|
||||
Self {
|
||||
http,
|
||||
api_url: api_url.into(),
|
||||
api_key: api_key.into(),
|
||||
#[cfg(feature = "mock")]
|
||||
mock: arc_swap::ArcSwapOption::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new Aditude client.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
api_url: impl Into<Cow<'static, str>>,
|
||||
api_key: impl Into<SecretString>,
|
||||
) -> Self {
|
||||
Self::from_client(reqwest::Client::new(), api_url, api_key)
|
||||
}
|
||||
|
||||
/// Creates an Aditude client which mocks responses.
|
||||
#[cfg(feature = "mock")]
|
||||
#[must_use]
|
||||
pub fn from_mock(mock: mock::AditudeMock) -> Self {
|
||||
Self {
|
||||
http: reqwest::Client::new(),
|
||||
api_url: "".into(),
|
||||
api_key: SecretString::from(String::new()),
|
||||
mock: arc_swap::ArcSwapOption::from_pointee(mock),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the mock responses that this client will output.
|
||||
#[cfg(feature = "mock")]
|
||||
pub fn set_mock(&self, mock: mock::AditudeMock) {
|
||||
self.mock.store(Some(std::sync::Arc::new(mock)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the start and end of what Aditude considers the "Yesterday" time
|
||||
/// range.
|
||||
#[must_use]
|
||||
pub fn yesterday(now: DateTime<Utc>) -> (DateTime<Utc>, DateTime<Utc>) {
|
||||
let start = DateTime::<Utc>::from_naive_utc_and_offset(
|
||||
(now - Duration::days(1))
|
||||
.date_naive()
|
||||
.and_hms_nano_opt(0, 0, 0, 0)
|
||||
.unwrap_or_default(),
|
||||
Utc,
|
||||
);
|
||||
let end = start + Duration::days(1);
|
||||
(start, end)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//! See [`AditudeMock`].
|
||||
|
||||
use crate::{v1, v2};
|
||||
|
||||
/// Mock data returned by [`crate::Aditude`].
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AditudeMock {
|
||||
/// Response for [`crate::Aditude::get_metrics_v1`].
|
||||
pub metrics_v1_response: Option<Vec<v1::MetricsResponse>>,
|
||||
/// Response for [`crate::Aditude::get_metrics_v2`].
|
||||
pub metrics_v2_response: Option<v2::Metrics>,
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use rust_decimal::Decimal;
|
||||
use secrecy::ExposeSecret;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::Client;
|
||||
|
||||
impl Client {
|
||||
/// Fetches insights metrics from the API.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Errors if the request could not be completed.
|
||||
pub async fn get_metrics_v1(
|
||||
&self,
|
||||
req: GetMetrics<'_>,
|
||||
) -> reqwest::Result<Vec<MetricsResponse>> {
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Body<'a> {
|
||||
pub metrics: &'a [MetricKind],
|
||||
pub range: &'static str,
|
||||
pub interval: Interval,
|
||||
#[serde(with = "chrono::serde::ts_milliseconds_option")]
|
||||
pub start_time: Option<DateTime<Utc>>,
|
||||
#[serde(with = "chrono::serde::ts_milliseconds_option")]
|
||||
pub end_time: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "mock")]
|
||||
if let Some(mock) = &*self.mock.load() {
|
||||
return Ok(mock.get_metrics_v1());
|
||||
}
|
||||
|
||||
let body = Body {
|
||||
metrics: req.metrics,
|
||||
range: match req.range {
|
||||
Range::Yesterday => "Yesterday",
|
||||
Range::Custom { .. } => "custom",
|
||||
},
|
||||
interval: req.interval,
|
||||
start_time: if let Range::Custom { start, .. } = req.range {
|
||||
Some(start)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
end_time: if let Range::Custom { end, .. } = req.range {
|
||||
Some(end)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
};
|
||||
|
||||
self.http
|
||||
.post(format!("{}/public/insights/metrics", self.api_url))
|
||||
.bearer_auth(self.api_key.expose_secret())
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<Vec<MetricsResponse>>()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GetMetrics<'a> {
|
||||
pub metrics: &'a [MetricKind],
|
||||
pub range: Range,
|
||||
pub interval: Interval,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Range {
|
||||
#[serde(rename = "Yesterday")]
|
||||
Yesterday,
|
||||
#[serde(rename = "custom", rename_all = "camelCase")]
|
||||
Custom {
|
||||
#[serde(with = "chrono::serde::ts_milliseconds")]
|
||||
start: DateTime<Utc>,
|
||||
#[serde(with = "chrono::serde::ts_milliseconds")]
|
||||
end: DateTime<Utc>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Interval {
|
||||
#[serde(rename = "1d")]
|
||||
OneDay,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum MetricKind {
|
||||
#[serde(rename = "METRIC_IMPRESSIONS")]
|
||||
Impressions,
|
||||
#[serde(rename = "METRIC_REVENUE")]
|
||||
Revenue,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MetricsResponse {
|
||||
pub points_list: Vec<Point>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Point {
|
||||
pub metric: Metric,
|
||||
pub time: Time,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Metric {
|
||||
#[serde(with = "rust_decimal::serde::float_option", default)]
|
||||
pub revenue: Option<Decimal>,
|
||||
pub impressions: Option<u128>,
|
||||
#[serde(with = "rust_decimal::serde::float_option", default)]
|
||||
pub cpm: Option<Decimal>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Time {
|
||||
pub seconds: u64,
|
||||
pub nanos: u64,
|
||||
}
|
||||
|
||||
#[cfg(feature = "mock")]
|
||||
const _: () = {
|
||||
use crate::{mock::AditudeMock, v1};
|
||||
|
||||
impl AditudeMock {
|
||||
pub(crate) fn get_metrics_v1(&self) -> Vec<v1::MetricsResponse> {
|
||||
self.metrics_v1_response
|
||||
.as_ref()
|
||||
.expect("missing mock `metrics_v1_response`")
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
pub use crate::v1::{Interval, Range};
|
||||
use chrono::{DateTime, Utc};
|
||||
use rust_decimal::Decimal;
|
||||
use secrecy::ExposeSecret;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::Client;
|
||||
|
||||
impl Client {
|
||||
/// Fetches insights metrics from the API.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Errors if the request could not be completed.
|
||||
pub async fn get_metrics_v2(
|
||||
&self,
|
||||
req: GetMetrics<'_>,
|
||||
) -> reqwest::Result<Metrics> {
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Body<'a> {
|
||||
pub metrics: &'a [MetricKind],
|
||||
pub range: &'static str,
|
||||
pub interval: Interval,
|
||||
#[serde(with = "chrono::serde::ts_milliseconds_option")]
|
||||
pub start_time: Option<DateTime<Utc>>,
|
||||
#[serde(with = "chrono::serde::ts_milliseconds_option")]
|
||||
pub end_time: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "mock")]
|
||||
if let Some(mock) = &*self.mock.load() {
|
||||
return Ok(mock.get_metrics_v2());
|
||||
}
|
||||
|
||||
let body = Body {
|
||||
metrics: req.metrics,
|
||||
range: match req.range {
|
||||
Range::Yesterday => "Yesterday",
|
||||
Range::Custom { .. } => "custom",
|
||||
},
|
||||
interval: req.interval,
|
||||
start_time: if let Range::Custom { start, .. } = req.range {
|
||||
Some(start)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
end_time: if let Range::Custom { end, .. } = req.range {
|
||||
Some(end)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
};
|
||||
|
||||
self.http
|
||||
.post(format!("{}/public/insights/metrics/v2", self.api_url))
|
||||
.bearer_auth(self.api_key.expose_secret())
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<Metrics>()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GetMetrics<'a> {
|
||||
pub metrics: &'a [MetricKind],
|
||||
pub range: Range,
|
||||
pub interval: Interval,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum MetricKind {
|
||||
#[serde(rename = "IMPRESSIONS")]
|
||||
Impressions,
|
||||
#[serde(rename = "REVENUE")]
|
||||
Revenue,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Metrics {
|
||||
pub responses: Vec<Response>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Response {
|
||||
pub rows: Vec<Row>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Row {
|
||||
#[serde(rename = "IMPRESSIONS")]
|
||||
pub impressions: Option<u128>,
|
||||
#[serde(rename = "REVENUE")]
|
||||
pub revenue: Option<Decimal>,
|
||||
#[serde(rename = "_TIME", with = "chrono::serde::ts_milliseconds")]
|
||||
pub time: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "mock")]
|
||||
const _: () = {
|
||||
use crate::{mock::AditudeMock, v2};
|
||||
|
||||
impl AditudeMock {
|
||||
pub(crate) fn get_metrics_v2(&self) -> v2::Metrics {
|
||||
self.metrics_v2_response
|
||||
.as_ref()
|
||||
.expect("missing mock `metrics_v2_response`")
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
#![expect(missing_docs, reason = "test crate")]
|
||||
|
||||
use aditude::{Client, mock::AditudeMock, v1, v2};
|
||||
use chrono::{DateTime, Utc};
|
||||
use rust_decimal::dec;
|
||||
|
||||
#[tokio::test]
|
||||
async fn mock_metrics() {
|
||||
let metrics_v1_response = vec![v1::MetricsResponse {
|
||||
points_list: vec![v1::Point {
|
||||
metric: v1::Metric {
|
||||
revenue: Some(dec!(1.23)),
|
||||
impressions: Some(456),
|
||||
cpm: Some(dec!(7.89)),
|
||||
},
|
||||
time: v1::Time {
|
||||
seconds: 123,
|
||||
nanos: 0,
|
||||
},
|
||||
}],
|
||||
}];
|
||||
|
||||
let epoch = DateTime::UNIX_EPOCH;
|
||||
let metrics_v2_response = v2::Metrics {
|
||||
responses: vec![
|
||||
v2::Response {
|
||||
rows: vec![v2::Row {
|
||||
impressions: Some(123),
|
||||
revenue: None,
|
||||
time: epoch,
|
||||
}],
|
||||
},
|
||||
v2::Response {
|
||||
rows: vec![v2::Row {
|
||||
impressions: None,
|
||||
revenue: Some(dec!(45.6)),
|
||||
time: epoch,
|
||||
}],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let aditude = Client::from_mock(AditudeMock {
|
||||
metrics_v1_response: Some(metrics_v1_response.clone()),
|
||||
metrics_v2_response: Some(metrics_v2_response.clone()),
|
||||
});
|
||||
assert_eq!(
|
||||
metrics_v1_response,
|
||||
aditude
|
||||
.get_metrics_v1(v1::GetMetrics {
|
||||
metrics: &[],
|
||||
range: v1::Range::Yesterday,
|
||||
interval: v1::Interval::OneDay,
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
metrics_v2_response,
|
||||
aditude
|
||||
.get_metrics_v2(v2::GetMetrics {
|
||||
metrics: &[],
|
||||
range: v2::Range::Yesterday,
|
||||
interval: v2::Interval::OneDay,
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_yesterday() {
|
||||
_ = dotenvy::dotenv();
|
||||
let (Ok(url), Ok(key)) = (
|
||||
dotenvy::var("ADITUDE_API_URL"),
|
||||
dotenvy::var("ADITUDE_API_KEY"),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
if url.trim().is_empty()
|
||||
|| key.trim().is_empty()
|
||||
|| url == "none"
|
||||
|| key == "none"
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let aditude = Client::new(url, key);
|
||||
|
||||
let real_yesterday = aditude
|
||||
.get_metrics_v2(v2::GetMetrics {
|
||||
metrics: &[v2::MetricKind::Impressions, v2::MetricKind::Revenue],
|
||||
range: v2::Range::Yesterday,
|
||||
interval: v2::Interval::OneDay,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Aditude defines the time range "Yesterday" according to this logic, I think.
|
||||
// We need to make sure it stays defined like this.
|
||||
let now = Utc::now();
|
||||
let (start_of_yesterday, end_of_yesterday) = aditude::yesterday(now);
|
||||
|
||||
let our_yesterday = aditude
|
||||
.get_metrics_v2(v2::GetMetrics {
|
||||
metrics: &[v2::MetricKind::Impressions, v2::MetricKind::Revenue],
|
||||
range: v2::Range::Custom {
|
||||
start: start_of_yesterday,
|
||||
end: end_of_yesterday,
|
||||
},
|
||||
interval: v2::Interval::OneDay,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(real_yesterday, our_yesterday);
|
||||
}
|
||||
Reference in New Issue
Block a user