Skip to main content

openrouter_rs/api/
analytics.rs

1use std::collections::HashMap;
2
3use derive_builder::Builder;
4use reqwest::Client as HttpClient;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8use crate::{
9    error::OpenRouterError,
10    strip_option_vec_setter,
11    transport::{request as transport_request, response as transport_response},
12    types::ApiResponse,
13};
14
15/// One analytics metric definition returned by `GET /analytics/meta`.
16#[derive(Serialize, Deserialize, Debug, Clone)]
17#[non_exhaustive]
18pub struct AnalyticsMetric {
19    pub name: String,
20    pub display_label: String,
21    pub is_rate: bool,
22    pub display_format: String,
23    #[serde(flatten)]
24    pub extra: HashMap<String, Value>,
25}
26
27/// One analytics dimension definition returned by `GET /analytics/meta`.
28#[derive(Serialize, Deserialize, Debug, Clone)]
29#[non_exhaustive]
30pub struct AnalyticsDimension {
31    pub name: String,
32    pub display_label: String,
33    #[serde(flatten)]
34    pub extra: HashMap<String, Value>,
35}
36
37/// One analytics filter operator definition returned by `GET /analytics/meta`.
38#[derive(Serialize, Deserialize, Debug, Clone)]
39#[non_exhaustive]
40pub struct AnalyticsOperator {
41    pub name: String,
42    pub value_type: String,
43    #[serde(flatten)]
44    pub extra: HashMap<String, Value>,
45}
46
47/// One analytics granularity definition returned by `GET /analytics/meta`.
48#[derive(Serialize, Deserialize, Debug, Clone)]
49#[non_exhaustive]
50pub struct AnalyticsGranularity {
51    pub name: String,
52    pub display_label: String,
53    #[serde(flatten)]
54    pub extra: HashMap<String, Value>,
55}
56
57/// Analytics query metadata.
58#[derive(Serialize, Deserialize, Debug, Clone)]
59#[non_exhaustive]
60pub struct AnalyticsMeta {
61    pub metrics: Vec<AnalyticsMetric>,
62    pub dimensions: Vec<AnalyticsDimension>,
63    pub operators: Vec<AnalyticsOperator>,
64    pub granularities: Vec<AnalyticsGranularity>,
65    #[serde(flatten)]
66    pub extra: HashMap<String, Value>,
67}
68
69/// Scalar value used inside analytics filters.
70#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
71#[serde(untagged)]
72#[non_exhaustive]
73pub enum AnalyticsFilterScalar {
74    String(String),
75    Number(f64),
76}
77
78impl From<&str> for AnalyticsFilterScalar {
79    fn from(value: &str) -> Self {
80        Self::String(value.to_string())
81    }
82}
83
84impl From<String> for AnalyticsFilterScalar {
85    fn from(value: String) -> Self {
86        Self::String(value)
87    }
88}
89
90impl From<f64> for AnalyticsFilterScalar {
91    fn from(value: f64) -> Self {
92        Self::Number(value)
93    }
94}
95
96/// Filter value accepted by analytics query requests.
97#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
98#[serde(untagged)]
99#[non_exhaustive]
100pub enum AnalyticsFilterValue {
101    String(String),
102    Number(f64),
103    Array(Vec<AnalyticsFilterScalar>),
104}
105
106/// One analytics filter.
107#[derive(Serialize, Deserialize, Debug, Clone)]
108pub struct AnalyticsFilter {
109    pub field: String,
110    pub operator: String,
111    pub value: AnalyticsFilterValue,
112}
113
114/// Analytics time range.
115#[derive(Serialize, Deserialize, Debug, Clone)]
116pub struct AnalyticsTimeRange {
117    pub start: String,
118    pub end: String,
119}
120
121/// Analytics ordering clause.
122#[derive(Serialize, Deserialize, Debug, Clone)]
123pub struct AnalyticsOrderBy {
124    pub field: String,
125    pub direction: String,
126}
127
128/// Request payload for `POST /analytics/query`.
129#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
130#[builder(build_fn(error = "OpenRouterError"))]
131#[non_exhaustive]
132pub struct AnalyticsQueryRequest {
133    #[builder(setter(custom))]
134    pub metrics: Vec<String>,
135    #[builder(setter(custom), default)]
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub dimensions: Option<Vec<String>>,
138    #[builder(setter(custom), default)]
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub filters: Option<Vec<AnalyticsFilter>>,
141    #[builder(setter(into, strip_option), default)]
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub granularity: Option<String>,
144    #[builder(setter(strip_option), default)]
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub group_limit: Option<u32>,
147    #[builder(setter(strip_option), default)]
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub limit: Option<u32>,
150    #[builder(setter(strip_option), default)]
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub order_by: Option<AnalyticsOrderBy>,
153    #[builder(setter(strip_option), default)]
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub time_range: Option<AnalyticsTimeRange>,
156}
157
158impl AnalyticsQueryRequest {
159    pub fn builder() -> AnalyticsQueryRequestBuilder {
160        AnalyticsQueryRequestBuilder::default()
161    }
162}
163
164impl AnalyticsQueryRequestBuilder {
165    pub fn metrics<T, S>(&mut self, items: T) -> &mut Self
166    where
167        T: IntoIterator<Item = S>,
168        S: Into<String>,
169    {
170        self.metrics = Some(items.into_iter().map(Into::into).collect());
171        self
172    }
173
174    strip_option_vec_setter!(dimensions, String);
175    strip_option_vec_setter!(filters, AnalyticsFilter);
176}
177
178/// Metadata returned with analytics query rows.
179#[derive(Serialize, Deserialize, Debug, Clone)]
180#[non_exhaustive]
181pub struct AnalyticsQueryMetadata {
182    pub query_time_ms: f64,
183    pub row_count: u64,
184    pub truncated: bool,
185    #[serde(flatten)]
186    pub extra: HashMap<String, Value>,
187}
188
189/// Analytics query result payload.
190#[derive(Serialize, Deserialize, Debug, Clone)]
191#[non_exhaustive]
192pub struct AnalyticsQueryResponse {
193    #[serde(default, rename = "cachedAt")]
194    pub cached_at: Option<f64>,
195    pub data: Vec<HashMap<String, Value>>,
196    pub metadata: AnalyticsQueryMetadata,
197    #[serde(default, skip_serializing_if = "Option::is_none")]
198    pub warnings: Option<Vec<String>>,
199    #[serde(flatten)]
200    pub extra: HashMap<String, Value>,
201}
202
203/// Get analytics query metadata (`GET /analytics/meta`).
204pub async fn get_analytics_meta(
205    base_url: &str,
206    management_key: &str,
207) -> Result<AnalyticsMeta, OpenRouterError> {
208    let http_client = crate::transport::new_client()?;
209    get_analytics_meta_with_client(&http_client, base_url, management_key).await
210}
211
212pub(crate) async fn get_analytics_meta_with_client(
213    http_client: &HttpClient,
214    base_url: &str,
215    management_key: &str,
216) -> Result<AnalyticsMeta, OpenRouterError> {
217    let url = format!("{base_url}/analytics/meta");
218    let response = transport_request::with_bearer_auth(
219        transport_request::get(http_client, &url),
220        management_key,
221    )
222    .send()
223    .await?;
224
225    if response.status().is_success() {
226        let payload: ApiResponse<AnalyticsMeta> =
227            transport_response::parse_json_response(response, "analytics meta").await?;
228        Ok(payload.data)
229    } else {
230        transport_response::handle_error(response).await?;
231        unreachable!()
232    }
233}
234
235/// Query analytics (`POST /analytics/query`).
236pub async fn query_analytics(
237    base_url: &str,
238    management_key: &str,
239    request: &AnalyticsQueryRequest,
240) -> Result<AnalyticsQueryResponse, OpenRouterError> {
241    let http_client = crate::transport::new_client()?;
242    query_analytics_with_client(&http_client, base_url, management_key, request).await
243}
244
245pub(crate) async fn query_analytics_with_client(
246    http_client: &HttpClient,
247    base_url: &str,
248    management_key: &str,
249    request: &AnalyticsQueryRequest,
250) -> Result<AnalyticsQueryResponse, OpenRouterError> {
251    let url = format!("{base_url}/analytics/query");
252    let response = transport_request::with_bearer_auth(
253        transport_request::post(http_client, &url),
254        management_key,
255    )
256    .json(request)
257    .send()
258    .await?;
259
260    if response.status().is_success() {
261        let payload: ApiResponse<AnalyticsQueryResponse> =
262            transport_response::parse_json_response(response, "analytics query").await?;
263        Ok(payload.data)
264    } else {
265        transport_response::handle_error(response).await?;
266        unreachable!()
267    }
268}